__getattr__ 在模块上
Posted
技术标签:
【中文标题】__getattr__ 在模块上【英文标题】:__getattr__ on a module 【发布时间】:2011-01-27 16:31:48 【问题描述】:如何在一个类、一个模块上实现 __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
【问题讨论】:
我可能会接受 grieve 的回答,因为它适用于所有情况(尽管它有点混乱,可以做得更好)。哈佛 S 和 S Lott 有很好的清晰答案,但它们不是实用的解决方案。 在你的情况下,你甚至没有进行属性访问,所以你同时要求两个不同的东西。所以主要的问题是你想要哪一个。您希望salutation
存在于全局或本地名称空间中(这是上面的代码试图做的),还是希望在对模块进行点访问时动态查找名称?这是两件不同的事情。
有趣的问题,你是怎么想到这个的?
Autoload in Python的可能重复
__getattr__
从 Python 3.7 开始支持模块
【参考方案1】:
您在这里遇到了两个基本问题:
__xxx__
方法仅在类中查找
TypeError: can't set attributes of built-in/extension type 'module'
(1) 意味着任何解决方案都必须跟踪正在检查的模块,否则 每个 模块将具有实例替换行为;和 (2) 意味着 (1) 甚至不可能......至少不是直接的。
幸运的是,sys.modules 对那里的内容并不挑剔,因此包装器将起作用,但仅用于模块访问(即import somemodule; somemodule.salutation('world')
;对于同模块访问,您几乎必须从替换类中提取方法并使用类上的自定义方法(我喜欢使用.export()
)或使用通用函数(例如那些已经列为答案的函数)将它们添加到globals()
eiher。要记住的一件事是:如果包装器正在创建一个每次都是新实例,而全局解决方案不是,您最终会得到微妙的不同行为。哦,您不能同时使用这两个实例——它是一个或另一个。
更新
来自Guido van Rossum:
实际上有一个 hack 偶尔会被使用和推荐:a 模块可以定义一个具有所需功能的类,然后在 最后,将 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,加载后。 (这不是偶然的。黑客是 很久以前提出的,我们决定我们足够喜欢它来支持它 进口机械。)
因此,实现您想要的既定方法是在您的模块中创建一个类,并作为模块的最后一幕,将 sys.modules[__name__]
替换为您的类的实例 - 现在您可以使用 @987654330 @/__setattr__
/__getattribute__
根据需要。
注意 1:如果您使用此功能,那么在进行 sys.modules
分配时,模块中的任何其他内容(例如全局变量、其他函数等)都将丢失 -- 所以 make确保所需的一切都在替换类中。
注意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()
。
【讨论】:
之所以有效,是因为导入机制正在积极启用此 hack,并且在其最后一步将实际模块从 sys.modules 中拉出后,加载后文档? 现在我觉得使用这个黑客更放心了,因为它是“半认可的”:) 这是在做一些古怪的事情,比如让import sys
给None
换成sys
。我猜这个 hack 在 Python 2 中没有被认可。
@asmeurer:要了解其原因(和解决方案),请参阅问题Why is the value of __name__ changing after assignment to sys.modules[__name__]?。
@Friedrich:是的。在我更新的答案中检查 Note 2。【参考方案2】:
A while ago, Guido declared that all special method lookups on
new-style classes bypass __getattr__
and __getattribute__
。 Dunder 方法以前曾在模块上工作过 - 例如,您可以通过在这些技巧之前定义 __enter__
和 __exit__
将模块用作上下文管理器 broke。
最近一些历史特性卷土重来,其中包括模块 __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。
【讨论】:
如果我通过m = ModuleType("mod")
动态创建模块并设置m.__getattr__ = lambda attr: return "foo"
;但是,当我运行from mod import foo
时,我得到TypeError: 'module' object is not iterable
。
@weberc2:设置为m.__getattr__ = lambda attr: "foo"
,另外您需要使用sys.modules['mod'] = m
为模块定义一个条目。之后,from mod import foo
没有错误。
wim:您还可以获得动态计算的值——比如拥有一个模块级属性——允许编写my_module.whatever
来调用它(在import my_module
之后)。
关于 Cython 对 PEP 562 的支持:定义模块函数 __getattr__
并使用它访问 globals()
,如 PEP 562 的第一个示例中所述,似乎可以按预期工作。据我了解,尚无法以 Cythonic 方式声明公共或只读变量,请参阅:github.com/cython/cython/issues/656 和 github.com/cython/cython/issues/3959,以及:cython.readthedocs.io/en/latest/src/changes.html(第 0.29.22 节,“其他更改”小节)。
【参考方案3】:
这是一个 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__])
【讨论】:
这可能行得通,但它可能不是作者真正问题的解决方案。 “可能有效”和“可能无效”不是很有帮助。这是一个 hack/trick,但它有效,并解决了问题提出的问题。 虽然这将在导入您的模块并访问其上不存在的属性的 other 模块中工作,但它不适用于此处的实际代码示例。访问 globals() 不会通过 sys.modules。 不幸的是,这不适用于当前模块,或者可能不适用于在import *
之后访问的内容。【参考方案4】:
我们通常不会那样做。
我们做的是这个。
class A(object):
....
# The implicit global instance
a= A()
def salutation( *arg, **kw ):
a.salutation( *arg, **kw )
为什么?使隐式全局实例可见。
例如,查看random
模块,它创建了一个隐式全局实例,以稍微简化您想要一个“简单”随机数生成器的用例。
【讨论】:
如果你真的有野心,你可以创建这个类,遍历它的所有方法,并为每个方法创建一个模块级函数。 @Paul Fisher:根据问题,该类已经存在。公开类的所有方法可能不是一个好主意。通常这些暴露的方法是“方便”的方法。并非所有都适用于隐式全局实例。【参考方案5】:类似于@Håvard S 提出的建议,如果我需要在模块上实现一些魔法(例如__getattr__
),我将定义一个继承自types.ModuleType
的新类并将其放入sys.modules
(可能会替换定义我的自定义ModuleType
的模块)。
请参阅Werkzeug 的主要__init__.py
文件,以了解相当健壮的实现。
【讨论】:
【参考方案6】:这有点骇人听闻,但是...
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 的回答,因为它看起来更干净,但这直接回答了问题。 这与我最终选择的更接近。这有点乱,但可以在同一个模块中正确使用 globals()。 在我看来,这个答案并不完全是“当调用模块的静态定义属性中不存在的函数时”的要求,因为它正在无条件地工作并添加所有可能的类方法。这可以通过使用模块包装器来解决,该模块包装器仅在有模块级别AttributeError
时才执行 AddGlobalAttribute()
- 这与 @Håvard S 的逻辑相反。如果我有机会我会对此进行测试并添加我自己的混合答案,即使 OP 已经接受了这个答案。
更新我之前的评论。我现在明白很难(不可能?)拦截全局(模块)命名空间的NameError
异常——这就解释了为什么这个答案会为它找到的每一种可能性添加可调用对象到当前的全局命名空间,以覆盖所有可能的情况时间。【参考方案7】:
这是我自己的谦虚贡献——对@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")
【讨论】:
【参考方案8】:创建包含类的模块文件。导入模块。在刚刚导入的模块上运行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')
【讨论】:
你在这里回答了一个不同的问题。以上是关于__getattr__ 在模块上的主要内容,如果未能解决你的问题,请参考以下文章
访问全局变量时模块的 __setattr__ 和 __getattr__
python中反射(__import__和getattr使用)