PEP 726 – 模块 __setattr__
和 __delattr__
- 作者:
- Sergey B Kirpichev <skirpichev at gmail.com>
- 发起人:
- Adam Turner <adam at python.org>
- 讨论至:
- Discourse 帖子
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建日期:
- 2023年8月24日
- Python 版本:
- 3.13
- 发布历史:
- 2023年4月6日, 2023年8月31日
- 决议:
- Discourse 消息
摘要
本 PEP 提议支持用户在模块上定义 __setattr__
和 __delattr__
方法,以扩展模块属性访问的自定义功能,超越 PEP 562。
动机
模块 __setattr__
有几种潜在用途:
- 完全阻止设置属性(即使其只读)
- 验证要分配的值
- 拦截设置属性并更新其他一些状态
对只读属性的适当支持还需要添加 __delattr__
函数以防止其被删除。
直接支持此类自定义将很方便,通过识别模块中定义的 __setattr__
和 __delattr__
方法,它们将像普通的 object.__setattr__()
和 object.__delattr__()
方法一样工作,但它们将在模块实例上定义。结合现有的 __getattr__
和 __dir__
方法,这将简化模块属性访问自定义的所有变体。
例如
# mplib.py
CONSTANT = 3.14
prec = 53
dps = 15
def dps_to_prec(n):
"""Return the number of bits required to represent n decimals accurately."""
return max(1, int(round((int(n)+1)*3.3219280948873626)))
def prec_to_dps(n):
"""Return the number of accurate decimals that can be represented with n bits."""
return max(1, int(round(int(n)/3.3219280948873626)-1))
def validate(n):
n = int(n)
if n <= 0:
raise ValueError('Positive integer expected')
return n
def __setattr__(name, value):
if name == 'CONSTANT':
raise AttributeError('Read-only attribute!')
if name == 'dps':
value = validate(value)
globals()['dps'] = value
globals()['prec'] = dps_to_prec(value)
return
if name == 'prec':
value = validate(value)
globals()['prec'] = value
globals()['dps'] = prec_to_dps(value)
return
globals()[name] = value
def __delattr__(name):
if name in ('CONSTANT', 'dps', 'prec'):
raise AttributeError('Read-only attribute!')
del globals()[name]
>>> import mplib
>>> mplib.foo = 'spam'
>>> mplib.CONSTANT = 42
Traceback (most recent call last):
...
AttributeError: Read-only attribute!
>>> del mplib.foo
>>> del mplib.CONSTANT
Traceback (most recent call last):
...
AttributeError: Read-only attribute!
>>> mplib.prec
53
>>> mplib.dps
15
>>> mplib.dps = 5
>>> mplib.prec
20
>>> mplib.dps = 0
Traceback (most recent call last):
...
ValueError: Positive integer expected
现有选项
目前的解决方法是将模块对象的 __class__
分配给 types.ModuleType
的自定义子类(参见 [1])。
例如,为了防止修改或删除属性,我们可以使用
# mod.py
import sys
from types import ModuleType
CONSTANT = 3.14
class ImmutableModule(ModuleType):
def __setattr__(name, value):
raise AttributeError('Read-only attribute!')
def __delattr__(name):
raise AttributeError('Read-only attribute!')
sys.modules[__name__].__class__ = ImmutableModule
但这种方法比提议的解决方案慢(约2倍)。更重要的是,它还会对属性访问的速度造成明显的衰减(约2-3倍)。
规范
模块级别的 __setattr__
函数应接受两个参数:属性名称和要分配的值,并返回 None
或引发 AttributeError
。
def __setattr__(name: str, value: typing.Any, /) -> None: ...
__delattr__
函数应接受一个参数:属性名称,并返回 None
或引发 AttributeError
。
def __delattr__(name: str, /) -> None: ...
__setattr__
和 __delattr__
函数将在模块的 __dict__
中查找。如果存在,则调用相应的函数来自定义属性的设置或删除,否则将使用正常机制(在模块字典中存储/删除值)。
定义模块 __setattr__
或 __delattr__
仅影响使用属性访问语法进行的查找——直接访问模块全局变量(无论是通过模块内的 globals()
,还是通过对模块全局字典的引用)不受影响。例如:
>>> import mod
>>> mod.__dict__['foo'] = 'spam' # bypasses __setattr__, defined in mod.py
或
# mod.py
def __setattr__(name, value):
...
foo = 'spam' # bypasses __setattr__
globals()['bar'] = 'spam' # here too
def f():
global x
x = 123
f() # and here
要使用模块全局变量并触发 __setattr__
(或 __delattr__
),可以在模块代码中通过 sys.modules[__name__]
访问它。
# mod.py
sys.modules[__name__].foo = 'spam' # bypasses __setattr__
def __setattr__(name, value):
...
sys.modules[__name__].bar = 'spam' # triggers __setattr__
此限制是故意的(就像 PEP 562 一样),因为解释器高度优化了模块全局变量的访问,并且禁用所有这些并进行 Python 编写的特殊方法将导致代码速度过慢,无法接受。
如何教授此内容
文档的“自定义模块属性访问”([1])部分将进行扩展,以包含新的函数。
参考实现
本 PEP 的参考实现可以在 CPython PR #108261 中找到。
向后兼容性
本 PEP 可能会破坏使用模块级别(全局)名称 __setattr__
和 __delattr__
的代码,但语言参考明确保留所有未记录的双下划线名称,并允许“无警告的破坏”[2]。
本 PEP 的性能影响很小,因为额外的字典查找比在字典中存储/删除值要便宜得多。而且很难想象有一个模块会期望用户频繁地设置(和/或删除)属性以至于成为性能问题。另一方面,提议的机制允许在不影响属性访问速度的情况下覆盖属性的设置/删除,这在可能导致性能下降的情况下更有可能。
讨论
正如 Victor Stinner 指出的那样,提议的 API 已经在标准库中有用,例如确保 sys.modules
的类型始终是 dict
>>> import sys
>>> sys.modules = 123
>>> import asyncio
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<frozen importlib._bootstrap>", line 1260, in _find_and_load
AttributeError: 'int' object has no attribute 'get'
或者防止删除关键的 sys
属性,这使得代码更加复杂。例如,使用 sys.stderr
的代码必须检查属性是否存在,并且它不是 None
。目前,可以删除任何 sys
属性,包括函数。
>>> import sys
>>> del sys.excepthook
>>> 1+ # notice the next line
sys.excepthook is missing
File "<stdin>", line 1
1+
^
SyntaxError: invalid syntax
有关其他详细信息,请参阅 相关问题。
其他标准库模块也带有可以(作为功能)覆盖的属性,并且在此处进行一些输入验证可能会有所帮助。示例:threading.excepthook
、warnings.showwarning
、io.DEFAULT_BUFFER_SIZE
或 os.SEEK_SET
。
自定义模块属性访问的一个典型用例是管理弃用警告。但是 PEP 562 仅部分解决了这种情况:例如,在尝试更改已重命名属性时,无法发出警告。
脚注
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0726.rst