PEP 726 – Modul __setattr__ und __delattr__
- Autor:
- Sergey B Kirpichev <skirpichev at gmail.com>
- Sponsor:
- Adam Turner <adam at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Erstellt:
- 24. Aug. 2023
- Python-Version:
- 3.13
- Post-History:
- 06. Apr. 2023, 31. Aug. 2023
- Resolution:
- Discourse-Nachricht
Zusammenfassung
Dieses PEP schlägt die Unterstützung benutzerdefinierter __setattr__ und __delattr__ Methoden für Module vor, um die Anpassung des Modulattributzugriffs über PEP 562 hinaus zu erweitern.
Motivation
Es gibt mehrere potenzielle Anwendungsfälle für ein Modul __setattr__
- Um das Setzen eines Attributs vollständig zu verhindern (d.h. es schreibgeschützt zu machen)
- Um den zuzuweisenden Wert zu validieren
- Um das Setzen eines Attributs abzufangen und einen anderen Zustand zu aktualisieren
Die ordnungsgemäße Unterstützung für schreibgeschützte Attribute würde auch das Hinzufügen der __delattr__ Funktion erfordern, um deren Löschung zu verhindern.
Es wäre praktisch, eine solche Anpassung direkt zu unterstützen, indem __setattr__ und __delattr__ Methoden, die in einem Modul definiert sind, erkannt werden, die wie normale object.__setattr__() und object.__delattr__() Methoden fungieren, außer dass sie auf Modulinstanzen definiert werden. Zusammen mit den bestehenden __getattr__ und __dir__ Methoden werden alle Varianten der Anpassung des Modulattributzugriffs gestrafft.
Zum Beispiel:
# 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('Positive 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]
>>> 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: Positive integer expected
Bestehende Optionen
Der aktuelle Workaround besteht darin, die __class__ eines Modulobjekts einer benutzerdefinierten Unterklasse von types.ModuleType zuzuweisen (siehe [1]).
Um beispielsweise die Änderung oder Löschung eines Attributs zu verhindern, könnten wir verwenden
# mod.py
import sys
from types import ModuleType
CONSTANT = 3.14
class ImmutableModule(ModuleType):
def __setattr__(name, value):
raise AttributeError('Read-only attribute!')
def __delattr__(name):
raise AttributeError('Read-only attribute!')
sys.modules[__name__].__class__ = ImmutableModule
Aber diese Variante ist langsamer (~2x) als die vorgeschlagene Lösung. Wichtiger ist, dass sie auch eine spürbare Geschwindigkeitsregression (~2-3x) für den Attributzugriff mit sich bringt.
Spezifikation
Die Funktion __setattr__ auf Modulebene sollte zwei Argumente akzeptieren: den Namen eines Attributs und den zuzuweisenden Wert, und None zurückgeben oder eine AttributeError auslösen.
def __setattr__(name: str, value: typing.Any, /) -> None: ...
Die Funktion __delattr__ sollte ein Argument akzeptieren: den Namen eines Attributs, und None zurückgeben oder eine AttributeError auslösen.
def __delattr__(name: str, /) -> None: ...
Die Funktionen __setattr__ und __delattr__ werden im Modul __dict__ gesucht. Wenn sie vorhanden sind, wird die entsprechende Funktion aufgerufen, um das Setzen oder Löschen des Attributs anzupassen, andernfalls funktioniert der normale Mechanismus (Speichern/Löschen des Werts im Modulverzeichnis).
Das Definieren von Modul __setattr__ oder __delattr__ betrifft nur Lookups, die über die Attributzugriffssyntax erfolgen – der direkte Zugriff auf globale Module (sei es über globals() innerhalb des Moduls oder über einen Verweis auf das globale Verzeichnis des Moduls) bleibt unberührt. Zum Beispiel
>>> import mod
>>> mod.__dict__['foo'] = 'spam' # bypasses __setattr__, defined in mod.py
oder
# mod.py
def __setattr__(name, value):
...
foo = 'spam' # bypasses __setattr__
globals()['bar'] = 'spam' # here too
def f():
global x
x = 123
f() # and here
Um ein Modul-Global zu verwenden und __setattr__ (oder __delattr__) auszulösen, kann man es über sys.modules[__name__] innerhalb des Modulcodes aufrufen.
# mod.py
sys.modules[__name__].foo = 'spam' # bypasses __setattr__
def __setattr__(name, value):
...
sys.modules[__name__].bar = 'spam' # triggers __setattr__
Diese Einschränkung ist beabsichtigt (genauso wie bei PEP 562), da der Interpreter den Zugriff auf globale Module stark optimiert und das Deaktivieren all dessen und das Durchlaufen spezieller in Python geschriebener Methoden den Code unakzeptabel verlangsamen würde.
Wie man das lehrt
Der Abschnitt „Anpassung des Modulattributzugriffs“ ([1]) der Dokumentation wird um die neuen Funktionen erweitert.
Referenzimplementierung
Die Referenzimplementierung für dieses PEP finden Sie in CPython PR #108261.
Abwärtskompatibilität
Dieses PEP kann Code brechen, der Modul-globale Namen __setattr__ und __delattr__ verwendet, aber die Sprachreferenz reserviert ausdrücklich *alle* undokumentierten Dunder-Namen und erlaubt „brechen ohne Vorwarnung“ [2].
Die Leistungsauswirkungen dieses PEP sind gering, da der zusätzliche Verzeichnisabruf viel billiger ist als das Speichern/Löschen des Werts im Verzeichnis. Außerdem ist es schwer vorstellbar, dass ein Modul erwartet, dass der Benutzer Attribute oft genug setzt (und/oder löscht, um ein Leistungsproblem darzustellen). Andererseits ermöglicht der vorgeschlagene Mechanismus das Überschreiben des Setzens/Löschens von Attributen, ohne die Geschwindigkeit des Attributzugriffs zu beeinträchtigen, was ein weitaus wahrscheinlicheres Szenario für eine Leistungseinbuße ist.
Diskussion
Wie von Victor Stinner hervorgehoben, könnte die vorgeschlagene API bereits in der Standardbibliothek nützlich sein, beispielsweise um sicherzustellen, dass der Typ von sys.modules immer ein dict ist.
>>> import sys
>>> sys.modules = 123
>>> import asyncio
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<frozen importlib._bootstrap>", line 1260, in _find_and_load
AttributeError: 'int' object has no attribute 'get'
oder um die Löschung kritischer sys-Attribute zu verhindern, was den Code komplizierter macht. Beispielsweise muss Code, der sys.stderr verwendet, prüfen, ob das Attribut existiert und ob es nicht None ist. Derzeit ist es möglich, jedes sys-Attribut zu entfernen, einschließlich Funktionen.
>>> import sys
>>> del sys.excepthook
>>> 1+ # notice the next line
sys.excepthook is missing
File "<stdin>", line 1
1+
^
SyntaxError: invalid syntax
Siehe verwandtes Issue für weitere Details.
Andere Standardbibliotheksmodule haben ebenfalls Attribute, die (als Feature) überschrieben werden können, und einige Eingabevalidierungen hier könnten hilfreich sein. Beispiele: threading.excepthook, warnings.showwarning, io.DEFAULT_BUFFER_SIZE oder os.SEEK_SET.
Auch ein typischer Anwendungsfall für die Anpassung des Modulattributzugriffs ist die Verwaltung von Deprecation-Warnungen. Aber PEP 562 erfüllt dieses Szenario nur teilweise: z.B. ist es unmöglich, eine Warnung auszugeben, wenn versucht wird, ein umbenanntes Attribut zu *ändern*.
Fußnoten
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0726.rst
Zuletzt geändert: 2025-08-08 15:00:59 GMT