|
| 1 | +PEP: 726 |
| 2 | +Title: Module ``__setattr__`` and ``__delattr__`` |
| 3 | +Author: Sergey B Kirpichev < [email protected]> |
| 4 | +Sponsor: Adam Turner < [email protected]> |
| 5 | +Discussions-To: https://discuss.python.org/t/32640/ |
| 6 | +Status: Draft |
| 7 | +Type: Standards Track |
| 8 | +Content-Type: text/x-rst |
| 9 | +Created: 24-Aug-2023 |
| 10 | +Python-Version: 3.13 |
| 11 | +Post-History: `06-Apr-2023 <https://discuss.python.org/t/25506/>`__, |
| 12 | + `31-Aug-2023 <https://discuss.python.org/t/32640/>`__, |
| 13 | + |
| 14 | + |
| 15 | +Abstract |
| 16 | +======== |
| 17 | + |
| 18 | +This PEP proposes supporting user-defined ``__setattr__`` |
| 19 | +and ``__delattr__`` methods on modules to extend customization |
| 20 | +of module attribute access beyond :pep:`562`. |
| 21 | + |
| 22 | +Motivation |
| 23 | +========== |
| 24 | + |
| 25 | +There are several potential uses of a module ``__setattr__``: |
| 26 | + |
| 27 | +1. To prevent setting an attribute at all (i.e. make it read-only) |
| 28 | +2. To validate the value to be assigned |
| 29 | +3. To intercept setting an attribute and update some other state |
| 30 | + |
| 31 | +Proper support for read-only attributes would also require adding the |
| 32 | +``__delattr__`` function to prevent their deletion. |
| 33 | + |
| 34 | +A typical workaround is assigning the ``__class__`` of a module object to a |
| 35 | +custom subclass of :py:class:`python:types.ModuleType` (see [1]_). |
| 36 | +Unfortunately, this also brings a noticeable speed regression |
| 37 | +(~2-3x) for attribute *access*. It would be convenient to directly |
| 38 | +support such customization, by recognizing ``__setattr__`` and ``__delattr__`` |
| 39 | +methods defined in a module that would act like normal |
| 40 | +:py:meth:`python:object.__setattr__` and :py:meth:`python:object.__delattr__` |
| 41 | +methods, except that they will be defined on module *instances*. |
| 42 | + |
| 43 | +For example |
| 44 | + |
| 45 | +.. code:: python |
| 46 | +
|
| 47 | + # mplib.py |
| 48 | +
|
| 49 | + CONSTANT = 3.14 |
| 50 | + prec = 53 |
| 51 | + dps = 15 |
| 52 | +
|
| 53 | + def dps_to_prec(n): |
| 54 | + """Return the number of bits required to represent n decimals accurately.""" |
| 55 | + return max(1, int(round((int(n)+1)*3.3219280948873626))) |
| 56 | +
|
| 57 | + def prec_to_dps(n): |
| 58 | + """Return the number of accurate decimals that can be represented with n bits.""" |
| 59 | + return max(1, int(round(int(n)/3.3219280948873626)-1)) |
| 60 | +
|
| 61 | + def validate(n): |
| 62 | + n = int(n) |
| 63 | + if n <= 0: |
| 64 | + raise ValueError('non-negative integer expected') |
| 65 | + return n |
| 66 | +
|
| 67 | + def __setattr__(name, value): |
| 68 | + if name == 'CONSTANT': |
| 69 | + raise AttributeError('Read-only attribute!') |
| 70 | + if name == 'dps': |
| 71 | + value = validate(value) |
| 72 | + globals()['dps'] = value |
| 73 | + globals()['prec'] = dps_to_prec(value) |
| 74 | + return |
| 75 | + if name == 'prec': |
| 76 | + value = validate(value) |
| 77 | + globals()['prec'] = value |
| 78 | + globals()['dps'] = prec_to_dps(value) |
| 79 | + return |
| 80 | + globals()[name] = value |
| 81 | +
|
| 82 | + def __delattr__(name): |
| 83 | + if name in ('CONSTANT', 'dps', 'prec'): |
| 84 | + raise AttributeError('Read-only attribute!') |
| 85 | + del globals()[name] |
| 86 | +
|
| 87 | +.. code:: pycon |
| 88 | +
|
| 89 | + >>> import mplib |
| 90 | + >>> mplib.foo = 'spam' |
| 91 | + >>> mplib.CONSTANT = 42 |
| 92 | + Traceback (most recent call last): |
| 93 | + ... |
| 94 | + AttributeError: Read-only attribute! |
| 95 | + >>> del mplib.foo |
| 96 | + >>> del mplib.CONSTANT |
| 97 | + Traceback (most recent call last): |
| 98 | + ... |
| 99 | + AttributeError: Read-only attribute! |
| 100 | + >>> mplib.prec |
| 101 | + 53 |
| 102 | + >>> mplib.dps |
| 103 | + 15 |
| 104 | + >>> mplib.dps = 5 |
| 105 | + >>> mplib.prec |
| 106 | + 20 |
| 107 | + >>> mplib.dps = 0 |
| 108 | + Traceback (most recent call last): |
| 109 | + ... |
| 110 | + ValueError: non-negative integer expected |
| 111 | +
|
| 112 | +
|
| 113 | +Specification |
| 114 | +============= |
| 115 | + |
| 116 | +The ``__setattr__`` function at the module level should accept two |
| 117 | +arguments, the name of an attribute and the value to be assigned, |
| 118 | +and return :py:obj:`None` or raise an :exc:`AttributeError`. |
| 119 | + |
| 120 | +.. code:: python |
| 121 | +
|
| 122 | + def __setattr__(name: str, value: typing.Any, /) -> None: ... |
| 123 | +
|
| 124 | +The ``__delattr__`` function should accept one argument, |
| 125 | +the name of an attribute, and return :py:obj:`None` or raise an |
| 126 | +:py:exc:`AttributeError`: |
| 127 | + |
| 128 | +.. code:: python |
| 129 | +
|
| 130 | + def __delattr__(name: str, /) -> None: ... |
| 131 | +
|
| 132 | +The ``__setattr__`` and ``__delattr__`` functions are looked up in the |
| 133 | +module ``__dict__``. If present, the appropriate function is called to |
| 134 | +customize setting the attribute or its deletion, else the normal |
| 135 | +mechanism (storing/deleting the value in the module dictionary) will work. |
| 136 | + |
| 137 | +Defining ``__setattr__`` or ``__delattr__`` only affect lookups made |
| 138 | +using the attribute access syntax---directly accessing the module |
| 139 | +globals is unaffected, e.g. ``sys.modules[__name__].some_global = 'spam'``. |
| 140 | + |
| 141 | + |
| 142 | +How to Teach This |
| 143 | +================= |
| 144 | + |
| 145 | +The "Customizing module attribute access" [1]_ section of the documentation |
| 146 | +will be expanded to include new functions. |
| 147 | + |
| 148 | + |
| 149 | +Reference Implementation |
| 150 | +======================== |
| 151 | + |
| 152 | +The reference implementation for this PEP can be found in `CPython PR #108261 |
| 153 | +<https://github.com/python/cpython/pull/108261>`__. |
| 154 | + |
| 155 | + |
| 156 | +Backwards compatibility |
| 157 | +======================= |
| 158 | + |
| 159 | +This PEP may break code that uses module level (global) names |
| 160 | +``__setattr__`` and ``__delattr__``, but the language reference |
| 161 | +explicitly reserves *all* undocumented dunder names, and allows |
| 162 | +"breakage without warning" [2]_. |
| 163 | + |
| 164 | +The performance implications of this PEP are small, since additional |
| 165 | +dictionary lookup is much cheaper than storing/deleting the value in |
| 166 | +the dictionary. Also it is hard to imagine a module that expects the |
| 167 | +user to set (and/or delete) attributes enough times to be a |
| 168 | +performance concern. On another hand, proposed mechanism allows to |
| 169 | +override setting/deleting of attributes without affecting speed of |
| 170 | +attribute access, which is much more likely scenario to get a |
| 171 | +performance penalty. |
| 172 | + |
| 173 | + |
| 174 | +Footnotes |
| 175 | +========= |
| 176 | + |
| 177 | +.. [1] Customizing module attribute access |
| 178 | + (https://docs.python.org/3.11/reference/datamodel.html#customizing-module-attribute-access) |
| 179 | +
|
| 180 | +.. [2] Reserved classes of identifiers |
| 181 | + (https://docs.python.org/3.11/reference/lexical_analysis.html#reserved-classes-of-identifiers) |
| 182 | +
|
| 183 | +
|
| 184 | +Copyright |
| 185 | +========= |
| 186 | + |
| 187 | +This document is placed in the public domain or under the |
| 188 | +CC0-1.0-Universal license, whichever is more permissive. |
0 commit comments