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

  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: ...

在模块的 __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.excepthookwarnings.showwarningio.DEFAULT_BUFFER_SIZEos.SEEK_SET

此外,自定义模块属性访问的典型用例是管理弃用警告。但是 PEP 562 仅部分实现了此场景:例如,在尝试 *更改* 重命名属性时无法发出警告。

脚注


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

上次修改时间:2024-02-28 23:47:57 GMT