PEP 726: Module __setattr__ and __delattr__ (#3301)
Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
This commit is contained in:
parent
f70b3ba7db
commit
43a28ef8b4
|
@ -602,6 +602,7 @@ pep-0721.rst @encukou
|
|||
pep-0722.rst @pfmoore
|
||||
pep-0723.rst @AA-Turner
|
||||
pep-0725.rst @pradyunsg
|
||||
pep-0726.rst @AA-Turner
|
||||
pep-0727.rst @JelleZijlstra
|
||||
# ...
|
||||
# pep-0754.txt
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
PEP: 726
|
||||
Title: Module ``__setattr__`` and ``__delattr__``
|
||||
Author: Sergey B Kirpichev <skirpichev@gmail.com>
|
||||
Sponsor: Adam Turner <python@quite.org.uk>
|
||||
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.
|
Loading…
Reference in New Issue