Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

PEP 726 – Module__setattr__ and__delattr__

Author:
Sergey B Kirpichev <skirpichev at gmail.com>
Sponsor:
Adam Turner <adam at python.org>
Discussions-To:
Discourse thread
Status:
Rejected
Type:
Standards Track
Created:
24-Aug-2023
Python-Version:
3.13
Post-History:
06-Apr-2023,31-Aug-2023
Resolution:
Discourse message

Table of Contents

Abstract

This PEP proposes supporting user-defined__setattr__and__delattr__ methods on modules to extend customizationof module attribute access beyondPEP 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.

It would be convenient to directly support such customization, by recognizing__setattr__ and__delattr__ methods defined in a module that would actlike normalobject.__setattr__() andobject.__delattr__() methods, except that they will be definedon moduleinstances. Together with existing__getattr__ and__dir__methods this will streamline all variants of customizing module attribute access.

For example

# mplib.pyCONSTANT=3.14prec=53dps=15defdps_to_prec(n):"""Return the number of bits required to represent n decimals accurately."""returnmax(1,int(round((int(n)+1)*3.3219280948873626)))defprec_to_dps(n):"""Return the number of accurate decimals that can be represented with n bits."""returnmax(1,int(round(int(n)/3.3219280948873626)-1))defvalidate(n):n=int(n)ifn<=0:raiseValueError('Positive integer expected')returnndef__setattr__(name,value):ifname=='CONSTANT':raiseAttributeError('Read-only attribute!')ifname=='dps':value=validate(value)globals()['dps']=valueglobals()['prec']=dps_to_prec(value)returnifname=='prec':value=validate(value)globals()['prec']=valueglobals()['dps']=prec_to_dps(value)returnglobals()[name]=valuedef__delattr__(name):ifnamein('CONSTANT','dps','prec'):raiseAttributeError('Read-only attribute!')delglobals()[name]
>>>importmplib>>>mplib.foo='spam'>>>mplib.CONSTANT=42Traceback (most recent call last):...AttributeError:Read-only attribute!>>>delmplib.foo>>>delmplib.CONSTANTTraceback (most recent call last):...AttributeError:Read-only attribute!>>>mplib.prec53>>>mplib.dps15>>>mplib.dps=5>>>mplib.prec20>>>mplib.dps=0Traceback (most recent call last):...ValueError:Positive integer expected

Existing Options

The current workaround is assigning the__class__ of a module object to acustom subclass oftypes.ModuleType (see[1]).

For example, to prevent modification or deletion of an attribute we could use:

# mod.pyimportsysfromtypesimportModuleTypeCONSTANT=3.14classImmutableModule(ModuleType):def__setattr__(name,value):raiseAttributeError('Read-only attribute!')def__delattr__(name):raiseAttributeError('Read-only attribute!')sys.modules[__name__].__class__=ImmutableModule

But this variant is slower (~2x) than the proposed solution. Moreimportantly, it also brings a noticeable speed regression (~2-3x) forattributeaccess.

Specification

The__setattr__ function at the module level should accept twoarguments, the name of an attribute and the value to be assigned,and returnNone or raise anAttributeError.

def__setattr__(name:str,value:typing.Any,/)->None:...

The__delattr__ function should accept one argument,the name of an attribute, and returnNone or raise anAttributeError:

def__delattr__(name:str,/)->None:...

The__setattr__ and__delattr__ functions are looked up in themodule__dict__. If present, the appropriate function is called tocustomize setting the attribute or its deletion, else the normalmechanism (storing/deleting the value in the module dictionary) will work.

Defining module__setattr__ or__delattr__ only affects lookups madeusing the attribute access syntax — directly accessing the module globals(whether byglobals() within the module, or via a reference to the module’sglobals dictionary) is unaffected. For example:

>>>importmod>>>mod.__dict__['foo']='spam'# bypasses __setattr__, defined in mod.py

or

# mod.pydef__setattr__(name,value):...foo='spam'# bypasses __setattr__globals()['bar']='spam'# here toodeff():globalxx=123f()# and here

To use a module global and trigger__setattr__ (or__delattr__),one can access it viasys.modules[__name__] within the module’s code:

# mod.pysys.modules[__name__].foo='spam'# bypasses __setattr__def__setattr__(name,value):...sys.modules[__name__].bar='spam'# triggers __setattr__

This limitation is intentional (just as for thePEP 562), because theinterpreter highly optimizes access to module globals and disabling all thatand going through special methods written in Python would slow down the codeunacceptably.

How to Teach This

The “Customizing module attribute access”[1] section of the documentationwill be expanded to include new functions.

Reference Implementation

The reference implementation for this PEP can be found inCPython PR #108261.

Backwards compatibility

This PEP may break code that uses module level (global) names__setattr__ and__delattr__, but the language referenceexplicitly reservesall undocumented dunder names, and allows“breakage without warning”[2].

The performance implications of this PEP are small, since additionaldictionary lookup is much cheaper than storing/deleting the value inthe dictionary. Also it is hard to imagine a module that expects theuser to set (and/or delete) attributes enough times to be aperformance concern. On another hand, proposed mechanism allows tooverride setting/deleting of attributes without affecting speed ofattribute access, which is much more likely scenario to get aperformance penalty.

Discussion

As pointed out by Victor Stinner, the proposed API could be useful already inthe stdlib, for example to ensure thatsys.modules type is always adict:

>>>importsys>>>sys.modules=123>>>importasyncioTraceback (most recent call last):  File"<stdin>", line1, in<module>  File"<frozen importlib._bootstrap>", line1260, in_find_and_loadAttributeError:'int' object has no attribute 'get'

or to prevent deletion of criticalsys attributes, which makes thecode more complicated. For example, code usingsys.stderr has tocheck if the attribute exists and if it’s notNone. Currently, it’spossible to remove anysys attribute, including functions:

>>>importsys>>>delsys.excepthook>>>1+# notice the next linesys.excepthook is missing File "<stdin>", line 1   1+    ^SyntaxError: invalid syntax

Seerelated issue forother details.

Other stdlib modules also come with attributes which can be overridden (as afeature) and some input validation here could be helpful. Examples:threading.excepthook,warnings.showwarning,io.DEFAULT_BUFFER_SIZE oros.SEEK_SET.

Also a typical use case for customizing module attribute access is managingdeprecation warnings. But thePEP 562 accomplishes this scenario onlypartially: e.g. it’s impossible to issue a warning during an attempt tochange a renamed attribute.

Footnotes

[1] (1,2)
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 theCC0-1.0-Universal license, whichever is more permissive.


Source:https://github.com/python/peps/blob/main/peps/pep-0726.rst

Last modified:2025-08-08 15:00:59 GMT


[8]ページ先頭

©2009-2025 Movatter.jp