Geschichte.
Vor Python 3.6 erforderte es, dass Deskriptoren, die Kenntnisse über ihren Attributnamen benötigten, benutzerdefinierte Metaklassen oder manuelle Klassendekoratoren verwendeten, um das Klassendictionary zu durchsuchen und Namen einzufügen. Dieser Ansatz war umständlich, fehleranfällig und verursachte Metaklassenkonflikte in komplexen Hierarchien. PEP 487 führte das __set_name__-Protokoll in Python 3.6 ein, um diesen Boilerplate-Code zu beseitigen, indem der Interpreter die Deskriptoren automatisch benachrichtigt.
Problem.
Eine Deskriptorklasse wird während der Ausführung des Klassenkörpers erstellt, hat jedoch zu diesem Zeitpunkt kein intrinsisches Wissen über den Variablennamen, an den sie gebunden ist, oder die Klasse, in der sie sich befindet. Diese Informationen sind unerlässlich, um aussagekräftige Fehlermeldungen zu generieren, Felder in ORM-Systemen zu registrieren oder Serialisierungsschemata zu erstellen. Ohne externe Benachrichtigung bleibt der Deskriptor anonym, was die Entwickler zwingt, den Attributnamen als String-Argument zu wiederholen, wodurch die DRY-Prinzipien verletzt werden.
Lösung.
Wenn type.__new__ eine Klasse erstellt, durchläuft es die Namensraumzuordnung, die von __prepare__ zurückgegeben wird. Für jeden Wert, der über eine __set_name__-Methode verfügt, ruft der Interpreter value.__set_name__(owner_class, attribute_name) auf. Diese Methode erhält die zu erstellende Klasse und den Attributstring, sodass der Deskriptor diese Metadaten speichern kann. Wenn jedoch ein Deskriptor nach Abschluss des Klassenerstellungsprozesses (Monkey-Patching) einem Klassenattribut zugewiesen wird, wird __set_name__ nicht automatisch aufgerufen, da die Typmaschine nicht mehr aktiv ist.
class TrackedDescriptor: def __set_name__(self, owner, name): self.owner = owner self.name = name def __get__(self, instance, owner): if instance is None: return self return f"{self.owner.__name__}.{self.name}" class Model: field = TrackedDescriptor() # Model.field.name == 'field' # Model.field.owner == Model
Kontext.
Bei der Entwicklung einer Bibliothek zur Konfigurationsverwaltung benötigten wir Deskriptoren, um Umgebungsvariablen darzustellen. Wenn ein Wert fehlte oder ungültig war, musste die Fehlermeldung den genauen Attributnamen in der Klasse angeben (z.B. Config.database_url is required), nicht nur eine allgemeine Nachricht.
Problem.
Ursprünglich mussten die Benutzer den Namen manuell angeben: database_url = EnvVar('database_url'). Dies führte zu Fehlern während des Refactorings, bei denen sich der String-Literal und der Variablenname unterschieden, was cryptische Laufzeitzweigerfehler verursachte.
Verschiedene in Betracht gezogene Lösungen:
Metaklasseninjektion. Wir implementierten ein ConfigMeta, das attrs inspizierte und attr.set_name(name) für jeden Deskriptor aufrief. Das funktionierte, zwang jedoch alle Benutzerklassen, von unserer Metaklasse zu erben, wodurch die Kompatibilität mit anderen Bibliotheken, die eigene Metaklassen wie abc.ABCMeta verwenden, beeinträchtigt wurde. Es fügte auch kognitive Überlastung für Benutzer hinzu, die mit Metaklassen nicht vertraut waren.
Klassen-Dekorator Patchen. Wir erstellten einen @config-Dekorator, der nach der Klassenerstellung über cls.__dict__ iterierte und Namen patchte. Dies vermied Metaklassenkonflikte, war jedoch optional; das Vergessen des Dekorators führte zu beschädigten Deskriptoren. Es lief auch nach der Klassenerstellung, sodass Deskriptoren ihre Namen während der __init_subclass__-Hooks nicht verwenden konnten, was die Introspektionsfähigkeiten einschränkte.
__set_name__-Protokoll. Wir fügten __set_name__ zu unserem EnvVar-Deskriptor hinzu. Dies erforderte keine Änderungen am Benutzercode, funktionierte automatisch während der Klassendefinition und erlaubte es dem Deskriptor, seinen Namen zu kennen, bevor __init_subclass__ abgeschlossen war, was eine frühzeitige Validierung ermöglichte.
Gewählte Lösung.
Wir haben __set_name__ übernommen, da es eine nullkostenfreie Abstraktion für Benutzer bot und sich nahtlos in Python's natives Datenmodell einfügte. Es beseitigte das Problem der Metaklassenkollision vollständig.
Ergebnis.
Die API wurde deklarativ: database_url = EnvVar(). Refactoring-Tools konnten Attribute sicher umbenennen, und Fehlermeldungen blieben genau. Der Code wurde um 150 Zeilen Metaklassen-Boilerplate reduziert, und wir beobachteten weniger Fehlermeldungen im Zusammenhang mit Konfigurationsschlüssel-Mismatches.
Wann genau wird __set_name__ während des Lebenszyklus der Klassenerstellung aufgerufen?
Es wird von type.__new__ unmittelbar nach Abschluss der Ausführung des Klassenkörpers und nachdem das Namensraum-Dictionary gefüllt wurde, jedoch bevor __init_subclass__ für übergeordnete Klassen aufgerufen wird. Dieser Zeitpunkt ist entscheidend, da er es den Deskriptoren ermöglicht, ihren Zustand abzuschließen, bevor Unterklassen initialisiert werden. Es wird nicht ausgelöst, wenn Attribute zu einer bereits erstellten Klasse hinzugefügt werden (z.B. setattr(MyClass, 'new_attr', descriptor())), da das Protokoll zur Klassenerstellung abgeschlossen ist. Dieses Verständnis ist entscheidend für die dynamische Klassenmanipulation.
Warum erhält __set_name__ sowohl die Eigentümerklasse als auch den Namen als Argumente, anstatt sie aus self abzuleiten?
Die Deskriptorinstanz existiert unabhängig von der Klasse; sie kann vor der Klassenerstellung instanziiert werden und theoretisch mehreren Klassen zugewiesen werden (obwohl dies selten ist). Das owner-Argument stellt sicher, dass der Deskriptor weiß, in welcher spezifischen Klasse die Zuweisung erfolgte, was notwendig ist, um Vererbung korrekt zu behandeln. Wenn ein Deskriptor in einer Basisklasse definiert ist, wird __set_name__ mit der Basisklasse aufgerufen; wenn er in einer Unterklasse mit einer neuen Instanz überschrieben wird, wird er mit der Unterklasse aufgerufen. Dies ermöglicht register pro Klasse ohne Übertragung zwischen Basis- und abgeleiteten Klassen.
Wie interagiert __set_name__ mit den Protokollmethoden __set__ und __get__ von Deskriptoren?
__set_name__ ist rein ein Initialisierungshook und nimmt nicht am Attributzugriffsprotokoll (__get__/__set__) teil. Es ermöglicht jedoch, dass diese Methoden korrekt funktionieren, indem es den Kontext bereitstellt, der für die Operationen erforderlich ist. Ein häufiger Fehler besteht darin, anzunehmen, dass __set_name__ erneut aufgerufen wird, wenn ein Deskriptor von einer Unterklasse geerbt wird, die ihn nicht überschreibt. Da dieselbe Deskriptorinstanz wiederverwendet wird, wird __set_name__ nicht erneut aufgerufen; daher müssen Deskriptoren, die ihren Status pro Klasse verfolgen, __init_subclass__ verwenden oder owner in __get__ überprüfen, um die Vererbung zu behandeln, anstatt sich ausschließlich auf __set_name__ für unterklassen-spezifische Logik zu verlassen.