-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PEP 726: Module __setattr__ and __delattr__ (#3301)
Co-authored-by: Adam Turner <[email protected]>
- Loading branch information
1 parent
f70b3ba
commit 43a28ef
Showing
2 changed files
with
189 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
PEP: 726 | ||
Title: Module ``__setattr__`` and ``__delattr__`` | ||
Author: Sergey B Kirpichev <[email protected]> | ||
Sponsor: Adam Turner <[email protected]> | ||
Discussions-To: https://discuss.python.org/t/32640/ | ||
Status: Draft | ||
Type: Standards Track | ||
Content-Type: text/x-rst | ||
Created: 24-Aug-2023 | ||
Python-Version: 3.13 | ||
Post-History: `06-Apr-2023 <https://discuss.python.org/t/25506/>`__, | ||
`31-Aug-2023 <https://discuss.python.org/t/32640/>`__, | ||
|
||
|
||
Abstract | ||
======== | ||
|
||
This PEP proposes supporting user-defined ``__setattr__`` | ||
and ``__delattr__`` methods on modules to extend customization | ||
of module attribute access beyond :pep:`562`. | ||
|
||
Motivation | ||
========== | ||
|
||
There are several potential uses of a module ``__setattr__``: | ||
|
||
1. To prevent setting an attribute at all (i.e. make it read-only) | ||
2. To validate the value to be assigned | ||
3. To intercept setting an attribute and update some other state | ||
|
||
Proper support for read-only attributes would also require adding the | ||
``__delattr__`` function to prevent their deletion. | ||
|
||
A typical workaround is assigning the ``__class__`` of a module object to a | ||
custom subclass of :py:class:`python:types.ModuleType` (see [1]_). | ||
Unfortunately, this also brings a noticeable speed regression | ||
(~2-3x) for attribute *access*. It would be convenient to directly | ||
support such customization, by recognizing ``__setattr__`` and ``__delattr__`` | ||
methods defined in a module that would act like normal | ||
:py:meth:`python:object.__setattr__` and :py:meth:`python:object.__delattr__` | ||
methods, except that they will be defined on module *instances*. | ||
|
||
For example | ||
|
||
.. code:: python | ||
# 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('non-negative 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] | ||
.. code:: pycon | ||
>>> 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: non-negative integer expected | ||
Specification | ||
============= | ||
|
||
The ``__setattr__`` function at the module level should accept two | ||
arguments, the name of an attribute and the value to be assigned, | ||
and return :py:obj:`None` or raise an :exc:`AttributeError`. | ||
|
||
.. code:: python | ||
def __setattr__(name: str, value: typing.Any, /) -> None: ... | ||
The ``__delattr__`` function should accept one argument, | ||
the name of an attribute, and return :py:obj:`None` or raise an | ||
:py:exc:`AttributeError`: | ||
|
||
.. code:: python | ||
def __delattr__(name: str, /) -> None: ... | ||
The ``__setattr__`` and ``__delattr__`` functions are looked up in the | ||
module ``__dict__``. If present, the appropriate function is called to | ||
customize setting the attribute or its deletion, else the normal | ||
mechanism (storing/deleting the value in the module dictionary) will work. | ||
|
||
Defining ``__setattr__`` or ``__delattr__`` only affect lookups made | ||
using the attribute access syntax---directly accessing the module | ||
globals is unaffected, e.g. ``sys.modules[__name__].some_global = 'spam'``. | ||
|
||
|
||
How to Teach This | ||
================= | ||
|
||
The "Customizing module attribute access" [1]_ section of the documentation | ||
will be expanded to include new functions. | ||
|
||
|
||
Reference Implementation | ||
======================== | ||
|
||
The reference implementation for this PEP can be found in `CPython PR #108261 | ||
<https://github.com/python/cpython/pull/108261>`__. | ||
|
||
|
||
Backwards compatibility | ||
======================= | ||
|
||
This PEP may break code that uses module level (global) names | ||
``__setattr__`` and ``__delattr__``, but the language reference | ||
explicitly reserves *all* undocumented dunder names, and allows | ||
"breakage without warning" [2]_. | ||
|
||
The performance implications of this PEP are small, since additional | ||
dictionary lookup is much cheaper than storing/deleting the value in | ||
the dictionary. Also it is hard to imagine a module that expects the | ||
user to set (and/or delete) attributes enough times to be a | ||
performance concern. On another hand, proposed mechanism allows to | ||
override setting/deleting of attributes without affecting speed of | ||
attribute access, which is much more likely scenario to get a | ||
performance penalty. | ||
|
||
|
||
Footnotes | ||
========= | ||
|
||
.. [1] Customizing module attribute access | ||
(https://docs.python.org/3.11/reference/datamodel.html#customizing-module-attribute-access) | ||
.. [2] Reserved classes of identifiers | ||
(https://docs.python.org/3.11/reference/lexical_analysis.html#reserved-classes-of-identifiers) | ||
Copyright | ||
========= | ||
|
||
This document is placed in the public domain or under the | ||
CC0-1.0-Universal license, whichever is more permissive. |