__getattr__ на модулі


127

Як можна реалізувати еквівалент __getattr__класу на модулі?

Приклад

Під час виклику функції, яка не існує в статично визначених атрибутах модуля, я хочу створити екземпляр класу в цьому модулі та викликати метод на ньому з тим самим іменем, що не вдався при пошуку атрибутів на модулі.

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

# note this function is intentionally on the module, and not the class above
def __getattr__(mod, name):
    return getattr(A(), name)

if __name__ == "__main__":
    # i hope here to have my __getattr__ function above invoked, since
    # salutation does not exist in the current namespace
    salutation("world")

Що дає:

matt@stanley:~/Desktop$ python getattrmod.py 
Traceback (most recent call last):
  File "getattrmod.py", line 9, in <module>
    salutation("world")
NameError: name 'salutation' is not defined

2
Я, швидше за все, пітиму з відповіддю скорботи, оскільки це працює в будь-яких обставинах (хоча це трохи безладно і може бути зроблено краще). Гарвард S і S Lott мають гарні чисті відповіді, але це не практичні рішення.
Метт Столяр

1
У вашому випадку ви навіть не робите доступ до атрибутів, тому ви просите відразу дві речі. Тож головне питання - який саме ти хочеш. Ви хочете salutationіснувати в глобальному чи локальному просторі імен (що намагається зробити код вище) або вам потрібно динамічний пошук імен, коли ви робите точковий доступ у модулі? Це дві різні речі.
Леннарт Регебро

Цікаве запитання, як ви придумали це?
Кріс Вессілінг

1
можливий дублікат Автозавантаження в Python
Piotr Dobrogost

1
__getattr__на модулях підтримується з Python 3.7
Fumito Hamamura

Відповіді:


42

Нещодавно Гвідо заявив, що всі спеціальні методи пошуку на класах нового стилю обходять __getattr__і__getattribute__ . Методи Дандера раніше працювали над модулями - ви, наприклад, можете використовувати модуль як менеджер контексту, просто визначившись __enter__і __exit__, перш ніж ці хитрощі розіб’ються .

Останнім часом деякі історичні особливості повернулися, модуль __getattr__серед них, і тому існуючий хак (модуль, що замінює себе класом в sys.modulesчас імпорту) вже не повинен бути необхідним.

У Python 3.7+ ви просто використовуєте один очевидний спосіб. Щоб налаштувати доступ до атрибутів на модулі, визначте __getattr__функцію на рівні модуля, яка повинна приймати один аргумент (ім'я атрибута) та повернути обчислене значення або підняти AttributeError:

# my_module.py

def __getattr__(name: str) -> Any:
    ...

Це також дозволить гакам імпорту "з", тобто ви можете повернути динамічно генеровані об'єкти для операторів, таких як from my_module import whatever.

У відповідній примітці, поряд з модулем getttr, ви також можете визначити __dir__функцію на рівні модуля, на яку слід відповідати dir(my_module). Докладніше див. PEP 562 .


1
Якщо я динамічно створю модуль через m = ModuleType("mod")і встановити m.__getattr__ = lambda attr: return "foo"; проте, коли бігаю from mod import foo, я отримую TypeError: 'module' object is not iterable.
weberc2

@ weberc2: Зробіть це m.__getattr__ = lambda attr: "foo", плюс вам потрібно визначити запис для модуля за допомогою sys.modules['mod'] = m. Згодом помилки з from mod import foo.
мартіно

wim: Ви також можете отримувати динамічно обчислені значення (наприклад, властивість рівня модуля), що дозволяє писати, my_module.whateverщоб викликати його (після ан import my_module).
мартіно

115

Тут зіткнулися дві основні проблеми:

  1. __xxx__ методи шукають лише на уроці
  2. TypeError: can't set attributes of built-in/extension type 'module'

(1) означає, що будь-яке рішення повинно також слідкувати за тим, який модуль досліджувався, інакше кожен модуль матиме поведінку підстановки примірника; і (2) означає, що (1) навіть неможливо ... принаймні не безпосередньо.

На щастя, sys.modules не є вибагливим щодо того, що відбувається там, щоб обгортка працювала, але тільки для доступу до модулів (тобто import somemodule; somemodule.salutation('world'); для доступу до цього ж модуля вам досить доведеться вибирати методи з класу заміни та додавати їх у globals()eiher за допомогою користувальницький метод у класі (мені подобається використовувати .export()) або із загальною функцією (наприклад, ті, що вже вказані як відповіді). Одне, що потрібно пам’ятати: якщо обгортка створює новий екземпляр кожного разу, а рішення глобальних програм - ні, Ви закінчуєте тонко різну поведінку. О, і вам не вдається використовувати обидва одночасно - це те чи інше.


Оновлення

Від Гідо ван Россума :

Насправді є хак, який періодично використовується та рекомендується: модуль може визначити клас із потрібною функціональністю, а потім, наприкінці, замінити себе в sys.modules екземпляром цього класу (або класом, якщо ви наполягаєте , але це взагалі менш корисно). Наприклад:

# module foo.py

import sys

class Foo:
    def funct1(self, <args>): <code>
    def funct2(self, <args>): <code>

sys.modules[__name__] = Foo()

Це працює, тому що імпортна техніка активно вмикає цей злом, і як його останній крок витягує фактичний модуль із sys.modules після його завантаження. (Це не випадково. Злом був запропонований давно, і ми вирішили, що нам сподобалося достатньо, щоб підтримати його в імпортній техніці.)

Таким чином, встановлений спосіб досягти того, що ви хочете, - це створити єдиний клас у своєму модулі, і як останній акт модуля замініть sys.modules[__name__]на екземпляр вашого класу - і тепер ви можете грати з __getattr__/ __setattr__/ __getattribute__за потребою.


Примітка 1. Якщо ви користуєтесь цією функціональністю, то все інше в модулі, наприклад глобалі, інші функції тощо, буде втрачено під час sys.modulesвиконання завдання, тому переконайтеся, що все необхідне знаходиться в класі заміщення.

Примітка 2 : Для підтримки from module import *ви повинні __all__визначитись у класі; наприклад:

class Foo:
    def funct1(self, <args>): <code>
    def funct2(self, <args>): <code>
    __all__ = list(set(vars().keys()) - {'__module__', '__qualname__'})

Залежно від вашої версії Python, можуть бути інші імена, які слід опустити __all__. set()Може бути опущена , якщо сумісність Python 2 не потрібно.


3
Це працює, тому що імпортна техніка активно вмикає цей злом, і як його останній крок витягує фактичний модуль із sys.modules, після його завантаження Чи згадується він десь у документах?
Пьотр Доброгост

4
Тепер я відчуваю себе легше, використовуючи цей хак, вважаючи його "напівсанкціонованим" :)
Mark Nunberg

3
Це робить Screwy речі, як зробити import sysподатливість Noneдля sys. Я здогадуюсь, цей хак не санкціонований у Python 2.
asmeurer

3
@asmeurer: Щоб зрозуміти причину цього (і рішення), див. питання Чому змінюється значення __name__ після призначення в sys.modules [__ name__]? .
мартіно

1
@qarma: Я, мабуть, згадую про деякі вдосконалення, про які говорилося, що дозволило б модулям python більш безпосередньо брати участь у моделі успадкування класів, але навіть цей метод все ще працює і підтримується.
Етан Фурман

48

Це злом, але ви можете обгорнути модуль класом:

class Wrapper(object):
  def __init__(self, wrapped):
    self.wrapped = wrapped
  def __getattr__(self, name):
    # Perform custom logic here
    try:
      return getattr(self.wrapped, name)
    except AttributeError:
      return 'default' # Some sensible default

sys.modules[__name__] = Wrapper(sys.modules[__name__])

10
приємно і брудно: D
Метт Столяр

Це може спрацювати, але це, мабуть, не є вирішенням реальної проблеми автора.
DasIch

12
"Може працювати" і "напевно, не" не дуже корисно. Це хак / трюк, але він працює і вирішує проблему, поставлену питанням.
Håvard S

6
Хоча це буде працювати в інших модулях, які імпортують ваш модуль та отримують доступ до неіснуючих атрибутів на ньому, він не працюватиме для фактичного прикладу коду тут. Доступ до глобальних мереж () не проходить через sys.modules.
Маріус Гедмінас

5
На жаль, це не працює для поточного модуля, або, ймовірно, для матеріалів, до яких можна отримати доступ після import *.
Метт Столяр

19

Ми зазвичай не робимо це так.

Що ми робимо, це і є.

class A(object):
....

# The implicit global instance
a= A()

def salutation( *arg, **kw ):
    a.salutation( *arg, **kw )

Чому? Так що видно неявний глобальний екземпляр.

Наприклад, подивіться на randomмодуль, який створює неявний глобальний екземпляр, щоб трохи спростити випадки використання, коли потрібно "простий" генератор випадкових чисел.


Якщо ви справді амбітні, ви можете створити клас та повторити всі його методи та створити функцію рівня модуля для кожного методу.
Пол Фішер

@Paul Fisher: Враховуючи проблему, клас вже існує. Викриття всіх методів класу може бути не дуже хорошою ідеєю. Зазвичай ці відкриті методи - це "зручні" методи. Не всі підходять для загальноприйнятого глобального екземпляра.
S.Lott

13

Аналогічно тому, що запропонував @ Håvard S, у випадку, коли мені потрібно було реалізувати якусь магію на модулі (на кшталт __getattr__), я визначив би новий клас, який успадковує types.ModuleTypeі додав би його sys.modules(можливо, замінивши модуль там, де ModuleTypeбуло визначено мій звичай ).

Дивіться головний __init__.pyфайл Werkzeug щодо досить надійної реалізації цього.


8

Це хакі, але ...

import types

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

    def farewell(self, greeting, accusative):
         print greeting, accusative

def AddGlobalAttribute(classname, methodname):
    print "Adding " + classname + "." + methodname + "()"
    def genericFunction(*args):
        return globals()[classname]().__getattribute__(methodname)(*args)
    globals()[methodname] = genericFunction

# set up the global namespace

x = 0   # X and Y are here to add them implicitly to globals, so
y = 0   # globals does not change as we iterate over it.

toAdd = []

def isCallableMethod(classname, methodname):
    someclass = globals()[classname]()
    something = someclass.__getattribute__(methodname)
    return callable(something)


for x in globals():
    print "Looking at", x
    if isinstance(globals()[x], (types.ClassType, type)):
        print "Found Class:", x
        for y in dir(globals()[x]):
            if y.find("__") == -1: # hack to ignore default methods
                if isCallableMethod(x,y):
                    if y not in globals(): # don't override existing global names
                        toAdd.append((x,y))


for x in toAdd:
    AddGlobalAttribute(*x)


if __name__ == "__main__":
    salutation("world")
    farewell("goodbye", "world")

Це працює за допомогою ітерації всіх об'єктів у глобальному просторі імен. Якщо елемент є класом, він повторює атрибути класу. Якщо атрибут називається, він додає його до глобального простору імен як функцію.

Він ігнорує всі атрибути, які містять "__".

Я б не використовував це у виробничому коді, але це повинно вас почати.


2
Я вважаю за краще відповідь Håvard S на мою, оскільки вона виглядає набагато чистішою, але це прямо відповідає на запитання.
скорботи

Це набагато ближче до того, з чим я врешті-решт пішов. Це трохи безладно, але правильно працює з глобалами () в межах одного модуля.
Метт Столяр

1
Мені здається, що ця відповідь не зовсім те, що було запропоновано, а саме: "Коли викликає функцію, яка не існує в статично визначених атрибутах модуля", тому що це працює беззастережно і додає всі можливі методи класу. Це можна виправити, скориставшись модульною обгорткою, яка виконує лише той рівень, AddGlobalAttribute()коли є рівень модуля AttributeError- вид зворотної логіки @ Håvard S. Якщо у мене є можливість, я перевірю це і додаю власну гібридну відповідь, хоча ОП вже прийняла цю відповідь.
мартіно

Оновити до мого попереднього коментаря. Тепер я розумію, що дуже важко (неможливо?) Перехоплювати NameErrorвинятки з глобального простору (модуля) імен - що пояснює, чому ця відповідь додає дзвінки для кожної можливості, яку вона виявить, до поточного глобального простору імен, щоб охопити кожен можливий випадок достроково.
мартіно

5

Ось мій власний скромний внесок - незначне прикрашення високо оціненої відповіді @ Håvard S, але трохи більш чітко (тому це може бути прийнятним для @ S.Lott, хоча, мабуть, недостатньо добре для ОП):

import sys

class A(object):
    def salutation(self, accusative):
        print "hello", accusative

class Wrapper(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __getattr__(self, name):
        try:
            return getattr(self.wrapped, name)
        except AttributeError:
            return getattr(A(), name)

_globals = sys.modules[__name__] = Wrapper(sys.modules[__name__])

if __name__ == "__main__":
    _globals.salutation("world")

-3

Створіть файл свого модуля, у якому є ваші класи. Імпортуйте модуль. Запустіть getattrмодуль, який ви щойно імпортували. Ви можете зробити динамічний імпорт, використовуючи __import__та витягуючи модуль із sys.modules.

Ось ваш модуль some_module.py:

class Foo(object):
    pass

class Bar(object):
    pass

І в іншому модулі:

import some_module

Foo = getattr(some_module, 'Foo')

Робити це динамічно:

import sys

__import__('some_module')
mod = sys.modules['some_module']
Foo = getattr(mod, 'Foo')

1
Ви тут відповідаєте на інше запитання.
Маріус Гедмінас
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.