__getattr__ в модуле
-
20-09-2019 - |
Вопрос
Как можно реализовать эквивалент __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
Решение
Некоторое время назад Guido объявил, что все запросы специальных методов в
классы нового стиля обходят __getattr__
и __getattribute__
.Методы Dunder ранее работали с модулями - вы могли бы, например, использовать модуль в качестве контекстного менеджера, просто определив __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)
.Видишь БОДРОСТЬ ДУХА 562 для получения подробной информации.
Другие советы
Есть две основные проблемы, с которыми вы сталкиваетесь здесь:
__xxx__
методы просматриваются только в классеTypeError: can't set attributes of built-in/extension type 'module'
(1) означает, что любое решение должно было бы также отслеживать, какой модуль проверялся, в противном случае каждый тогда модуль будет иметь поведение подстановки экземпляра;и (2) означает, что (1) даже невозможно...по крайней мере, не напрямую.
К счастью, sys.modules не придирчив к тому, что туда входит, поэтому оболочка будет работать, но только для доступа к модулю (т.е. import somemodule; somemodule.salutation('world')
;для доступа к одному модулю вам в значительной степени придется извлечь методы из класса подстановки и добавить их в globals()
либо с помощью пользовательского метода в классе (мне нравится использовать .export()
) или с помощью универсальной функции (например, те, которые уже перечислены в качестве ответов).Одна вещь, которую нужно иметь в виду:если оболочка каждый раз создает новый экземпляр, а глобальное решение - нет, в конечном итоге вы получите слегка отличающееся поведение.О, и вы не можете использовать и то, и другое одновременно - это либо одно, либо другое.
Обновить
От Гвидо ван Россум:
На самом деле существует хак, который иногда используется и рекомендуется:a модуль может определить класс с желаемой функциональностью, а затем в конце заменить себя в 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
назначение выполнено - поэтому убедитесь, что все необходимое есть внутри замещающего класса.
Это взлом, но вы можете обернуть модуль классом:
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__])
Обычно мы так не поступаем.
Вот что мы делаем.
class A(object):
....
# The implicit global instance
a= A()
def salutation( *arg, **kw ):
a.salutation( *arg, **kw )
Почему?Так что неявный глобальный экземпляр виден.
Для примеров посмотрите на random
модуль, который создает неявный глобальный экземпляр, чтобы немного упростить варианты использования, когда вам нужен "простой" генератор случайных чисел.
Аналогично тому, что предложил @Håvard S, в случае, когда мне нужно было реализовать некоторую магию в модуле (например __getattr__
), я бы определил новый класс, который наследуется от types.ModuleType
и вложи это в sys.modules
(вероятно, замена модуля, где мой пользовательский ModuleType
был определен).
Смотрите основные __init__.py
файл из Werkzeug для довольно надежной реализации этого.
Это халтурно, но...
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")
Это работает путем перебора всех объектов в глобальном пространстве имен.Если элемент является классом, он выполняет итерацию по атрибутам класса.Если атрибут можно вызвать, он добавляет его в глобальное пространство имен в виде функции.
Он игнорирует все атрибуты, которые содержат "__".
Я бы не стал использовать это в производственном коде, но это должно помочь вам начать.
Вот мой собственный скромный вклад - небольшое приукрашивание высоко оцененного ответа @Håvard S, но немного более явного (так что это может быть приемлемо для @S.Лотт, хотя, вероятно, недостаточно хорош для ОПЕРАЦИИ):
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")
Создайте свой модульный файл, содержащий ваши классы.Импортируйте модуль.Беги 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')
В некоторых обстоятельствах globals()
словаря может быть достаточно, например, вы можете создать экземпляр класса по имени из глобальной области видимости:
from somemodule import * # imports SomeClass
someclass_instance = globals()['SomeClass']()