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-0722.rst @pfmoore
|
||||||
pep-0723.rst @AA-Turner
|
pep-0723.rst @AA-Turner
|
||||||
pep-0725.rst @pradyunsg
|
pep-0725.rst @pradyunsg
|
||||||
|
pep-0726.rst @AA-Turner
|
||||||
pep-0727.rst @JelleZijlstra
|
pep-0727.rst @JelleZijlstra
|
||||||
# ...
|
# ...
|
||||||
# pep-0754.txt
|
# 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