模块中的__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")
这样就得到了:
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
9 个回答
这其实是个小技巧,你可以用一个类来包装这个模块:
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__])
你在这里遇到的主要问题有两个:
__xxx__
方法只在类上查找TypeError: can't set attributes of built-in/extension type 'module'
第一个问题意味着,任何解决方案都必须跟踪正在检查哪个模块,否则每个模块都会有实例替换的行为;而第二个问题则意味着第一个问题甚至不可能解决……至少不能直接解决。
幸运的是,sys.modules 对于存放的内容并不挑剔,所以可以使用一个包装器,但这只适用于模块访问(也就是说,像 import somemodule; somemodule.salutation('world')
这样的用法);如果要在同一个模块内访问,你几乎必须从替换类中提取方法,并将它们添加到 globals()
中,可以通过在类上定义一个自定义方法(我喜欢使用 .export()
)或者使用一个通用函数(比如已经列出的那些答案)。需要记住的一点是:如果包装器每次都创建一个新实例,而 globals 解决方案没有这样做,你最终会得到微妙不同的行为。哦,还有,你不能同时使用两者——只能选一个。
更新
来自 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()
之所以可行,是因为导入机制主动支持这个技巧,并且在加载模块后,最后一步会将实际模块从 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__
中省略。如果不需要兼容 Python 2,可以省略 set()
。
之前,Guido(Python 的创始人)宣布,新的类在查找特殊方法时,会跳过 __getattr__
和 __getattribute__
这两个方法。以前,特殊方法可以在模块上使用,比如你可以通过定义 __enter__
和 __exit__
来让一个模块作为上下文管理器使用,但后来这些方法出现了一些问题。
最近,一些历史特性又回来了,其中就包括模块的 __getattr__
方法,因此之前的那种黑科技(在导入时用一个类替换模块)就不再需要了。
在 Python 3.7 及以上版本中,你只需要用一种简单的方法来实现。要自定义模块的属性访问,只需在模块级别定义一个 __getattr__
函数,这个函数应该接受一个参数(属性的名字),并返回计算出的值,或者抛出一个 AttributeError
错误:
# my_module.py
def __getattr__(name: str) -> Any:
...
这样做还可以让你在使用 "from" 导入时进行一些处理,也就是说,你可以为像 from my_module import whatever
这样的语句返回动态生成的对象。
另外,除了模块的 getattr
,你还可以在模块级别定义一个 __dir__
函数,以响应 dir(my_module)
的调用。有关详细信息,请查看 PEP 562。