140

如何__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
4

8 回答 8

124

您在这里遇到两个基本问题:

  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())或通用函数(例如那些已经列为答案的函数)。要记住的一件事:如果包装器每次都创建一个新实例,而全局解决方案不是,你最终会得到微妙的不同行为。哦,你不能同时使用两者——这是一个或另一个。


更新

Guido van Rossum

实际上有一个偶尔使用和推荐的技巧:一个模块可以定义一个具有所需功能的类,然后最后在 sys.modules 中将其自身替换为该类的实例(或者如果您坚持使用该类) ,但这通常不太有用)。例如:

# module foo.py

import sys

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

sys.modules[__name__] = Foo()

这是有效的,因为导入机制正在积极地启用这个 hack,并且作为它的最后一步,在加载后将实际模块从 sys.modules 中拉出。(这绝非偶然。该 hack 是很久以前提出的,我们决定我们很喜欢在进口机器中支持它。)

因此,完成您想要的既定方法是在您的模块中创建一个单独的类,并作为模块的最后一个动作替换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 兼容性,则可以省略。

于 2011-10-05T21:59:51.303 回答
62

不久前,Guido 宣布所有新式类的特殊方法查找都会绕过__getattr____getattribute__. Dunder 方法以前曾在模块上工作过——例如,在这些技巧失效__enter__之前,您可以简单地通过定义and来将模块用作上下文管理器。__exit__

最近一些历史特性卷土重来,其中的模块__getattr__,因此现有的 hack(在导入时用类替换自身的模块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

于 2018-02-21T21:58:56.100 回答
50

这是一个 hack,但你可以用一个类包装模块:

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__])
于 2010-03-15T13:24:22.480 回答
20

我们通常不会那样做。

我们要做的就是这个。

class A(object):
....

# The implicit global instance
a= A()

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

为什么?这样隐式全局实例是可见的。

例如,查看random模块,它创建了一个隐式全局实例,以稍微简化您想要一个“简单”随机数生成器的用例。

于 2010-03-15T13:25:57.313 回答
13

类似于@Håvard S 提出的建议,在我需要在模块上实现一些魔法的情况下(例如),我将定义一个继承自并将其放入__getattr__的新类(可能替换定义我的自定义的模块)。types.ModuleTypesys.modulesModuleType

请参阅Werkzeug的主__init__.py文件,以获得相当健壮的实现。

于 2010-03-15T15:02:49.987 回答
9

这很骇人听闻,但是...

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

这是通过迭代全局命名空间中的所有对象来实现的。如果项目是一个类,它会遍历类属性。如果属性是可调用的,它会将其作为函数添加到全局命名空间中。

它忽略所有包含“__”的属性。

我不会在生产代码中使用它,但它应该让你开始。

于 2010-05-05T20:35:45.197 回答
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")
于 2013-07-12T22:33:56.250 回答
-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')
于 2010-03-15T15:09:00.473 回答