__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
Скорее всего, я воспользуюсь ответом grieve, поскольку он работает при любых обстоятельствах (хотя он немного запутан, и его можно было бы сделать лучше). У Harvard S и S Lott есть хорошие ясные ответы, но это не практические решения.
Мэтт Джойнер
1
В вашем случае вы даже не делаете доступ к атрибутам, поэтому вы запрашиваете сразу две разные вещи. Итак, главный вопрос - какой именно вам нужен. Вы хотите salutationсуществовать в глобальном или локальном пространстве имен (это то, что пытается сделать приведенный выше код) или вам нужен динамический поиск имен, когда вы делаете доступ через точку к модулю? Это разные вещи.
Леннарт Регебро
Интересный вопрос, как вы к этому пришли?
Крис Весселинг,
1
возможный дубликат автозагрузки в Python
Петр Доброгост
1
__getattr__на модулях поддерживается Python 3.7
Фумито Хамамура

Ответы:

42

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

Wim
источник
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()любой другой с помощью пользовательский метод в классе (мне нравится использовать .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
Теперь я чувствую себя более непринужденно, используя этот хак, считая его «полусанкционированным» :)
Марк Нунберг
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__])
Håvard S
источник
10
красиво и грязно: D
Мэтт Джойнер
Это может сработать, но, вероятно, это не решение настоящей проблемы автора.
DasIch
12
«Может сработать» и «вероятно, нет» не очень помогают. Это хитрость / уловка, но она работает и решает проблему, поставленную вопросом.
Håvard S
6
Хотя это будет работать в других модулях, которые импортируют ваш модуль и получают доступ к несуществующим атрибутам в нем, это не сработает для реального примера кода здесь. Доступ к globals () не осуществляется через sys.modules.
Мариус Гедминас
5
К сожалению, это не работает для текущего модуля или, скорее всего, для вещей, доступ к которым осуществляется после import *.
Мэтт Джойнер
19

Обычно мы так не поступаем.

Вот что мы делаем.

class A(object):
....

# The implicit global instance
a= A()

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

Зачем? Чтобы неявный глобальный экземпляр был виден.

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

С. Лотт
источник
Если вы действительно амбициозны, вы можете создать класс, перебрать все его методы и создать функцию уровня модуля для каждого метода.
Пол Фишер
@ Пол Фишер: Согласно проблеме, класс уже существует. Показывать все методы класса - не лучшая идея. Обычно эти открытые методы являются "удобными". Не все подходят для неявного глобального экземпляра.
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
Я предпочитаю ответ Ховарда С моему, так как он кажется намного чище, но он дает прямой ответ на заданный вопрос.
скорблю
Это намного ближе к тому, что я в итоге выбрал. Это немного беспорядочно, но корректно работает с globals () в том же модуле.
Мэтт Джойнер
1
Мне кажется, что этот ответ - это не совсем то, о чем спрашивали, а именно: «При вызове функции, которая не существует в статически определенных атрибутах модуля», потому что он выполняет свою работу безоговорочно и добавляет все возможные методы класса. Это можно исправить, используя оболочку модуля, которая работает только тогда, AddGlobalAttribute()когда есть уровень модуля AttributeError- что-то вроде обратной логики @Håvard S. Если у меня будет возможность, я протестирую это и добавлю свой собственный гибридный ответ, хотя OP уже принял этот ответ.
Мартино
Обновление моего предыдущего комментария. Теперь я понимаю, что очень сложно (невозможно?) Перехватить NameErrorисключения для глобального (модульного) пространства имен - что объясняет, почему этот ответ добавляет вызываемые объекты для каждой возможности, которую он находит в текущем глобальном пространстве имен, чтобы охватить все возможные случаи заранее.
Мартино
5

Вот мой скромный вклад - небольшое приукрашивание высоко оцененного ответа @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")
Мартино
источник
-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
Здесь вы отвечаете на другой вопрос.
Мариус Гедминас,