python-peps/pep-0726.rst

227 lines
6.4 KiB
ReStructuredText

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 module ``__setattr__`` or ``__delattr__`` only affects lookups made
using the attribute access syntax---directly accessing the module globals
(whether by ``globals()`` within the module, or via a reference to the module's
globals dictionary) is unaffected. For example:
.. code:: pycon
>>> import mod
>>> mod.__dict__['foo'] = 'spam' # bypasses __setattr__, defined in mod.py
or
.. code:: python
# mod.py
def __setattr__(name, value):
...
foo = 'spam' # bypasses __setattr__
globals()['bar'] = 'spam' # here too
def f():
global x
x = 123
f() # and here
To use a module global and trigger ``__setattr__`` (or ``__delattr__``),
one can access it via ``sys.modules[__name__]`` within the module's code:
.. code:: python
# mod.py
sys.modules[__name__].foo = 'spam' # bypasses __setattr__
def __setattr__(name, value):
...
sys.modules[__name__].bar = 'spam' # triggers __setattr__
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.