PEP 713 – 可调用模块
- 作者:
- Amethyst Reese <amethyst at n7.gg>
- 发起人:
- Łukasz Langa <lukasz at python.org>
- 讨论至:
- Discourse 帖子
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建日期:
- 2023年4月20日
- Python 版本:
- 3.12
- 发布历史:
- 2023年4月23日
- 决议:
- Discourse 消息
拒绝通知
指导委员会认为没有充分的理由提出此 PEP,尽管从一致性的角度来看,这显然是可以实现的。如果将来再次出现此想法,这是一个有用的先前讨论可供参考。
摘要
模块目前不能直接调用。类可以定义一个 __call__ 方法,使实例对象可调用,但在全局模块范围内定义一个同名函数没有效果,并且只能通过导入或直接将其引用为 module.__call__ 来调用该函数。PEP 562 添加了对模块的 __getattr__() 和 __dir__() 的支持,但定义 __getattr__ 以返回 __call__ 的值仍然不能使模块可调用。
此 PEP 提议通过在模块的全局命名空间中定义一个 __call__ 对象来支持使模块直接可调用,该对象可以是标准函数,也可以是任意可调用对象。
动机
许多模块只有一个主要功能接口。在许多情况下,该接口是一个可调用对象,能够直接导入和使用模块作为可调用对象,为用户提供了更“Pythonic”的接口。
# user.py
import fancy
@fancy
def func(...):
...
目前,提供这种风格的接口需要修改模块对象以使其可调用。
这通常通过用可调用替代项(例如函数或类实例)替换 sys.modules 中的模块对象来完成。
# fancy.py
def fancy(...):
...
sys.modules[__name__] = fancy
这使得原始模块在没有作者进一步钩子的情况下实际上无法访问,即使使用 from module import member。它还会导致一个“模块”对象,该对象缺少所有特殊的模块属性,包括 __doc__、__package__、__path__ 等。
或者,模块作者可以选择用提供可调用接口的自定义类型覆盖模块的 __class__ 属性。
# fancy.py
def fancy(...):
...
class FancyModule(types.ModuleType):
def __call__(self, ...):
return fancy(...)
sys.modules[__name__].__class__ = FancyModule
这两种方法的缺点是,它们不仅会导致额外的样板代码,还会导致类型检查器出现故障,因为它们无法识别模块在运行时是可调用的。
$ mypy user.py
user.py:3: error: Module not callable [operator]
Found 1 error in 1 file (checked 1 source file)
规范
当模块对象被调用,并且找到 __call__ 对象(无论是通过 __getattr__ 还是 __dict__ 查找的结果)时,该对象将被给定参数调用。
如果找不到 __call__ 对象,则会引发 TypeError,这与现有行为一致。
所有这些示例都将被视为有效的、可调用的模块。
# hello.py
def __call__(...):
pass
# hello.py
class Hello:
pass
__call__ = Hello
# hello.py
def hello():
pass
def __getattr__(name):
if name == "__call__":
return hello
前两种样式通常应优先考虑,因为它们允许类型检查器等工具进行更轻松的静态分析,尽管第三种形式将被允许以使实现更加一致。
目的是允许将任意可调用对象分配给模块的 __call__ 属性或由模块的 __getattr__ 方法返回,从而使模块作者能够选择最适合的机制来使他们的模块可供用户调用。
向后兼容性和对性能的影响
此 PEP 预计不会引起任何向后不兼容。任何已经包含 __call__ 对象的模块将继续正常运行,只是增加了直接调用的能力。不太可能存在现有 __call__ 对象并依赖于调用时引发 TypeError 的现有行为的模块。
此 PEP 的性能影响很小,因为它定义了一个新接口。调用模块将触发对模块对象上 __call__ 名称的查找。创建可调用模块的现有解决方法已经依赖于对通用对象执行此行为,从而为这些可调用模块产生类似的性能。
类型检查器很可能需要相应地更新,才能将具有 __call__ 对象的模块视为可调用。在检查针对不支持可调用模块的旧 Python 版本的代码时,可以在类型检查器中支持这一点,并且预期这些模块也将包含前面提到的解决方法之一来使模块可调用。
如何教授此内容
可调用类型的文档(callable types)将模块包含在列表中,并链接到 __call__()(__call__())。有关模拟可调用对象(Emulating callable objects)的文档将包含一个涵盖可调用模块的部分,并附带示例代码,类似于自定义模块属性访问(customizing module attribute access)部分。
参考实现
可调用模块的建议实现可在 CPython PR #103742 中找到。
被拒绝的想法
考虑到 __getattr__ 和 __dir__ 的引入,以及启用 __call__ 使用的提议,人们考虑了是否值得允许对模块使用所有特殊方法名称,例如 __or__ 和 __iter__。虽然这并非完全不受欢迎,但它增加了向后兼容性问题的可能性,并且与其他特殊方法相比,这些特殊方法可能为库作者提供的效用较小。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0713.rst