PEP 726 – 模块 __setattr__
和 __delattr__
- 作者:
- Sergey B Kirpichev <skirpichev at gmail.com>
- 赞助者:
- Adam Turner <python at quite.org.uk>
- 讨论列表:
- 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: ...
在模块的 __dict__
中查找 __setattr__
和 __delattr__
函数。如果存在,则调用相应的函数来自定义属性的设置或删除,否则将使用正常机制(在模块字典中存储/删除值)。
定义模块 __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
上次修改时间:2024-02-28 23:47:57 GMT