Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 562 – 模块 __getattr__ 和 __dir__

作者:
Ivan Levkivskyi <levkivskyi at gmail.com>
状态:
最终版
类型:
标准跟踪
创建日期:
2017年9月9日
Python 版本:
3.7
发布历史:
2017年9月9日
决议:
Python-Dev 消息

目录

重要

本PEP是一份历史文档。最新的、权威的文档现在可以在 自定义模块属性访问 中找到。

×

有关如何提出更改,请参阅 PEP 1

摘要

提议支持在模块上定义的 __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