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

Python 增强提案

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__ 有几种潜在用途:

  1. 完全阻止设置属性(即使其只读)
  2. 验证要分配的值
  3. 拦截设置属性并更新其他一些状态

对只读属性的适当支持还需要添加 __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.excepthookwarnings.showwarningio.DEFAULT_BUFFER_SIZEos.SEEK_SET

自定义模块属性访问的一个典型用例是管理弃用警告。但是 PEP 562 仅部分解决了这种情况:例如,在尝试更改已重命名属性时,无法发出警告。

脚注


来源:https://github.com/python/peps/blob/main/peps/pep-0726.rst

最后修改:2025-08-08 15:00:59 GMT