__getattr__ на модуле

Как реализовать эквивалент __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")

Что дает:

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

Ответ 1

Некоторое время назад Гвидо объявил, что все специальные методы поиска на классы нового стиля обходят __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.

В соответствующей заметке вместе с модулем getattr вы также можете определить функцию __dir__ на уровне модуля для ответа на dir(my_module). Подробнее см. PEP 562.

Ответ 2

Здесь есть две основные проблемы:

  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() или с пользовательским методом в классе (мне нравится использовать .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__ по мере необходимости.

Обратите внимание, что если вы используете эту функцию, все остальное в модуле, например, глобальные переменные, другие функции и т.д., Будет потеряно при назначении sys.modules - поэтому убедитесь, что все необходимое находится внутри класса замены.

Ответ 3

Это взломать, но вы можете обернуть модуль классом:

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__])

Ответ 4

Обычно мы этого не делаем.

Мы делаем это.

class A(object):
....

# The implicit global instance
a= A()

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

Почему? Таким образом, видится неявный глобальный экземпляр.

В качестве примеров рассмотрим модуль random, который создает неявный глобальный экземпляр, чтобы немного упростить варианты использования, в которых вы хотите создать "простой" генератор случайных чисел.

Ответ 5

Подобно тому, что предложил Х. Ховард S в случае, когда мне нужно было реализовать некоторую магию в модуле (например, __getattr__), я бы определил новый класс, наследующий от types.ModuleType, и поместил его в sys.modules (возможно, заменив модуль, где был определен мой пользовательский ModuleType).

См. основной файл __init__.py Werkzeug для довольно надежной реализации этого.

Ответ 6

Это хаки, но...

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")

Это работает путем итерации по всем объектам глобального пространства имен. Если элемент является классом, он выполняет итерацию над атрибутами класса. Если атрибут является вызываемым, он добавляет его в глобальное пространство имен как функцию.

Он игнорирует все атрибуты, содержащие "__".

Я бы не использовал это в производственном коде, но он должен вас запустить.

Ответ 7

Вот мой собственный скромный вклад - небольшое приукрашивание высоко оцененного ответа @Håvard S, но немного более явное (поэтому это может быть приемлемо для @S.Lott, хотя, вероятно, и не достаточно для OP):

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")

Ответ 8

Создайте свой файл модуля с вашими классами. Импортируйте модуль. Запустите 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')

Ответ 9

В некоторых случаях словарь globals() может быть достаточным, например, вы можете создать экземпляр класса по имени из глобальной области:

from somemodule import * # imports SomeClass

someclass_instance = globals()['SomeClass']()