模块中的__getattr__

150 投票
9 回答
71656 浏览
提问于 2025-04-15 20:27

如何在一个模块中实现类似于类中的 __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 个回答

51

这其实是个小技巧,你可以用一个类来包装这个模块:

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

你在这里遇到的主要问题有两个:

  1. __xxx__ 方法只在类上查找
  2. 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()

77

之前,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

撰写回答