PEP 562 – 模块 __getattr__ 和 __dir__
- 作者:
- Ivan Levkivskyi <levkivskyi at gmail.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2017年9月9日
- Python 版本:
- 3.7
- 发布历史:
- 2017年9月9日
- 决议:
- 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__
的代码。(但语言参考明确保留了 所有 未文档化的双下划线名称,并允许“不经警告的破坏”;请参阅 [3]。)本PEP对性能的影响很小,因为 __getattr__
只会在属性缺失时被调用。
一些执行模块属性发现的工具可能不期望 __getattr__
。然而,这个问题并非新问题,因为已经可以通过用重写了 __getattr__
和 __dir__
的模块子类替换模块来实现,但有了本PEP,此类问题可能会更频繁地发生。
讨论
请注意,使用模块 __getattr__
需要小心,以保持所引用的对象可被pickle化。例如,函数的 __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
最后修改时间:2025-02-01 08:55:40 GMT