PEP 562 – 模块 __getattr__ 和 __dir__
- 作者:
- Ivan Levkivskyi <levkivskyi at gmail.com>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2017-09-09
- Python 版本:
- 3.7
- 历史记录:
- 2017-09-09
- 决议:
- Python-Dev 消息
摘要
建议支持在模块上定义的 __getattr__
和 __dir__
函数,以提供模块属性访问的基本自定义。
基本原理
有时需要自定义或以其他方式控制对模块属性的访问。一个典型的例子是管理弃用警告。典型的解决方法是将模块对象的 __class__
分配给 types.ModuleType
的自定义子类,或用自定义包装器实例替换 sys.modules
项目。通过识别直接在模块中定义的 __getattr__
来简化此过程会很方便,该函数的行为类似于普通的 __getattr__
方法,但它将在模块实例上定义。例如
# lib.py
from warnings import warn
deprecated_names = ["old_function", ...]
def _deprecated_old_function(arg, other):
...
def __getattr__(name):
if name in deprecated_names:
warn(f"{name} is deprecated", DeprecationWarning)
return globals()[f"_deprecated_{name}"]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
# main.py
from lib import old_function # Works, but emits the warning
另一个广泛使用 __getattr__
的用例是延迟子模块导入。考虑一个简单的例子
# lib/__init__.py
import importlib
__all__ = ['submod', ...]
def __getattr__(name):
if name in __all__:
return importlib.import_module("." + name, __name__)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
# lib/submod.py
print("Submodule loaded")
class HeavyClass:
...
# main.py
import lib
lib.submod.HeavyClass # prints "Submodule loaded"
有一个相关的提案 PEP 549,建议支持实例属性以实现类似的功能。区别在于,本 PEP 提出了一个更快、更简单的机制,但提供了更基本的自定义。此提案的另一个动机是 PEP 484 已经在 Python 存根文件中为此目的定义了模块 __getattr__
的用法,请参阅 PEP 484。
此外,为了允许修改对模块调用 dir()
的结果,以显示已弃用和其他动态生成的属性,建议支持模块级别的 __dir__
函数。例如
# lib.py
deprecated_names = ["old_function", ...]
__all__ = ["new_function_one", "new_function_two", ...]
def new_function_one(arg, other):
...
def new_function_two(arg, other):
...
def __dir__():
return sorted(__all__ + deprecated_names)
# main.py
import lib
dir(lib) # prints ["new_function_one", "new_function_two", "old_function", ...]
规范
模块级别的 __getattr__
函数应接受一个参数,该参数是属性的名称,并返回计算出的值或引发 AttributeError
def __getattr__(name: str) -> Any: ...
如果通过正常查找(即 object.__getattribute__
)未在模块对象上找到属性,则在引发 AttributeError
之前,将在模块 __dict__
中搜索 __getattr__
。如果找到,则使用属性名称调用它,并返回结果。将名称查找为模块全局变量将绕过模块 __getattr__
。这是故意的,否则对于内置函数调用 __getattr__
将显着损害性能。
__dir__
函数不应接受任何参数,并返回一个字符串列表,表示模块上可访问的名称
def __dir__() -> List[str]: ...
如果存在,此函数将覆盖模块上的标准 dir()
搜索。
此 PEP 的参考实现可以在 [2] 中找到。
向后兼容性和对性能的影响
此 PEP 可能会破坏使用模块级(全局)名称 __getattr__
和 __dir__
的代码。(但语言参考明确保留了所有未记录的 dunder 名称,并允许“无需警告即可破坏”;请参阅 [3]。)此 PEP 的性能影响最小,因为 __getattr__
仅在缺少属性时才会被调用。
一些执行模块属性发现的工具可能不会期望 __getattr__
。但是,这个问题并不新鲜,因为已经可以将模块替换为具有覆盖的 __getattr__
和 __dir__
的模块子类,但是使用此 PEP,此类问题可能会更频繁地发生。
讨论
请注意,使用模块 __getattr__
需要小心,以保持引用的对象可腌制。例如,函数的 __name__
属性应与可以通过 __getattr__
访问它的名称相对应。
def keep_pickleable(func):
func.__name__ = func.__name__.replace('_deprecated_', '')
func.__qualname__ = func.__qualname__.replace('_deprecated_', '')
return func
@keep_pickleable
def _deprecated_old_function(arg, other):
...
还应该注意避免递归,就像使用类级 __getattr__
一样。
要使用触发 __getattr__
的模块全局变量(例如,如果要使用延迟加载的子模块),可以将其访问为
sys.modules[__name__].some_global
或作为
from . import some_global
请注意,后者设置了模块属性,因此 __getattr__
将仅被调用一次。
参考文献
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0562.rst
上次修改时间:2023-09-09 17:39:29 GMT