Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 520 – Beibehaltung der Reihenfolge von Klassenattributdefinitionen

Autor:
Eric Snow <ericsnowcurrently at gmail.com>
Status:
Final
Typ:
Standards Track
Erstellt:
07-Jun-2016
Python-Version:
3.6
Post-History:
07-Jun-2016, 11-Jun-2016, 20-Jun-2016, 24-Jun-2016
Resolution:
Python-Dev Nachricht

Inhaltsverzeichnis

Hinweis

Seitdem kompakte Dictionaries in 3.6 implementiert wurden, wurde __definition_order__ entfernt. cls.__dict__ leistet nun größtenteils dasselbe.

Zusammenfassung

Die Klassendefinitionssyntax ist naturgemäß geordnet. Dort definierte Klassenattribute sind daher geordnet. Abgesehen von der Lesbarkeit ist diese Ordnung manchmal von Bedeutung. Wenn sie außerhalb der Klassendefinition automatisch verfügbar wäre, könnte die Attributreihenfolge ohne zusätzlichen Boilerplate-Code (wie Metaklassen oder manuelles Aufzählen der Attributreihenfolge) verwendet werden. Da diese Informationen bereits existieren, ist der Zugriff auf die Definitionsreihenfolge von Attributen eine vernünftige Erwartung. Derzeit beibehält Python jedoch nicht die Attributreihenfolge aus der Klassendefinition.

Diese PEP ändert dies, indem sie die Reihenfolge beibehält, in der Attribute im Klassendefinitionskörper eingeführt werden. Diese Reihenfolge wird nun im Attribut __definition_order__ der Klasse gespeichert. Dies ermöglicht die Introspektion der ursprünglichen Definitionsreihenfolge, z. B. durch Klassendekoratoren.

Zusätzlich erfordert diese PEP, dass der standardmäßige Klassendefinitions-Namespace standardmäßig geordnet ist (z. B. OrderedDict). Der langlebige Klassen-Namespace (__dict__) bleibt ein dict.

Motivation

Die Attributreihenfolge aus einer Klassendefinition kann für Tools nützlich sein, die auf die Namensreihenfolge angewiesen sind. Ohne die automatische Verfügbarkeit der Definitionsreihenfolge müssen diese Tools jedoch zusätzliche Anforderungen an die Benutzer stellen. Beispielsweise kann die Verwendung eines solchen Tools erfordern, dass Ihre Klasse eine bestimmte Metaklasse verwendet. Solche Anforderungen reichen oft aus, um die Verwendung des Tools zu entmutigen.

Einige Tools, die von dieser PEP profitieren könnten, sind

  • Dokumentationsgeneratoren
  • Test-Frameworks
  • CLI-Frameworks
  • Web-Frameworks
  • Konfigurationsgeneratoren
  • Daten-Serialisierer
  • Enum-Fabriken (meine ursprüngliche Motivation)

Hintergrund

Wenn eine Klasse mit einer class-Anweisung definiert wird, wird der Klassenkörper innerhalb eines Namespaces ausgeführt. Derzeit ist dieser Namespace standardmäßig dict. Wenn die Metaklasse __prepare__() definiert, wird das Ergebnis des Aufrufs für den Klassendefinitions-Namespace verwendet.

Nach Abschluss der Ausführung wird der Definitions-Namespace in ein neues dict kopiert. Dann wird der ursprüngliche Definitions-Namespace verworfen. Die neue Kopie wird als Klassen-Namespace gespeichert und über einen schreibgeschützten Proxy als __dict__ freigegeben.

Die Reihenfolge der Klassendefinitionsattribute wird durch die Einfügungsreihenfolge der Namen im *Definitions*-Namespace dargestellt. Somit können wir auf die Definitionsreihenfolge zugreifen, indem wir den Definitions-Namespace in eine geordnete Zuordnung ändern, wie z. B. collections.OrderedDict. Dies ist mithilfe einer Metaklasse und __prepare__ möglich, wie oben beschrieben. Tatsächlich ist genau dies der mit Abstand häufigste Anwendungsfall für die Verwendung von __prepare__.

Zu diesem Zeitpunkt fehlt nur noch, sie in der Klasse zu speichern, bevor der Definitions-Namespace verworfen wird. Auch dies kann mithilfe einer Metaklasse erfolgen. Dies bedeutet jedoch, dass die Definitionsreihenfolge nur für Klassen beibehalten wird, die eine solche Metaklasse verwenden. Es gibt zwei praktische Probleme damit:

Erstens erfordert es die Verwendung einer Metaklasse. Metaklassen führen eine zusätzliche Komplexitätsebene in den Code ein und sind in einigen Fällen (z. B. Konflikte) ein Problem. Daher ist es lohnenswert, die Notwendigkeit zu reduzieren, wenn sich die Gelegenheit bietet. PEP 422 – Einfachere Anpassung der Klassenerstellung und PEP 487 – Einfachere Anpassung der Klassenerstellung diskutieren dies ausführlich. Wir haben eine solche Gelegenheit, indem wir eine geordnete Zuordnung (z. B. OrderedDict für CPython zumindest) für den standardmäßigen Klassendefinitions-Namespace verwenden, wodurch __prepare__() praktisch überflüssig wird.

Zweitens haben nur Klassen, die sich für die Verwendung der OrderedDict-basierten Metaklasse entscheiden, Zugriff auf die Definitionsreihenfolge. Dies ist problematisch in Fällen, in denen ein universeller Zugriff auf die Definitionsreihenfolge wichtig ist.

Spezifikation

Teil 1

  • Alle Klassen haben ein Attribut __definition_order__
  • __definition_order__ ist ein Tupel von Bezeichnern (oder None)
  • __definition_order__ ist immer gesetzt
    1. Während der Ausführung des Klassenkörpers wird die Einfügungsreihenfolge der Namen in den *Definitions*-Namespace in einem Tupel gespeichert
    2. Wenn __definition_order__ im Klassenkörper definiert ist, muss es ein Tupel von Bezeichnern oder None sein; jeder andere Wert führt zu einem TypeError
    3. Klassen, die keine Klassendefinition haben (z. B. Builtins), haben ihr __definition_order__ auf None gesetzt
    4. Klassen, für die __prepare__() etwas anderes als OrderedDict (oder eine Unterklasse) zurückgegeben hat, haben ihr __definition_order__ auf None gesetzt (außer wo #2 gilt)

Nichts wird geändert

  • dir() wird nicht von __definition_order__ abhängen
  • Deskriptoren und benutzerdefinierte __getattribute__-Methoden sind bezüglich __definition_order__ unbeschränkt

Teil 2

  • Der standardmäßige Klassendefinitions-Namespace ist nun eine geordnete Zuordnung (z. B. OrderedDict)
  • cls.__dict__ ändert sich nicht und bleibt ein schreibgeschützter Proxy um dict

Beachten Sie, dass Python-Implementierungen, die ein geordnetes dict haben, nichts ändern müssen.

Der folgende Code demonstriert annähernd äquivalente Semantiken für beide Teile 1 und 2

class Meta(type):
    @classmethod
    def __prepare__(cls, *args, **kwargs):
        return OrderedDict()

class Spam(metaclass=Meta):
    ham = None
    eggs = 5
    __definition_order__ = tuple(locals())

Warum ein Tupel?

Die Verwendung eines Tupels spiegelt die Tatsache wider, dass wir die Reihenfolge der Attribute auf der Klasse offenlegen, wie sie *definiert* wurden. Da die Definition bereits abgeschlossen ist, wenn __definition_order__ gesetzt wird, werden Inhalt und Reihenfolge des Werts nicht mehr geändert. Daher verwenden wir einen Typ, der diesen Zustand der Unveränderlichkeit kommuniziert.

Warum kein schreibgeschütztes Attribut?

Es gibt einige gültige Argumente dafür, __definition_order__ als schreibgeschütztes Attribut zu machen (ähnlich wie cls.__dict__). Am bemerkenswertesten ist, dass ein schreibgeschütztes Attribut die Natur des Attributs als „vollständig“ vermittelt, was für __definition_order__ genau richtig ist. Da es den Zustand eines bestimmten einmaligen Ereignisses (Ausführung des Klassendefinitionskörpers) darstellt, würde das Ersetzen des Werts das Vertrauen verringern, dass das Attribut dem ursprünglichen Klassenkörper entspricht. Darüber hinaus hilft ein standardmäßig unveränderlicher Ansatz oft, Daten besser verständlich zu machen.

In diesem Fall gibt es jedoch immer noch keinen *starken* Grund, die etablierte Präzedenzfall in Python zu widersprechen. Laut Guido

I don't see why it needs to be a read-only attribute. There are
very few of those -- in general we let users play around with
things unless we have a hard reason to restrict assignment (e.g.
the interpreter's internal state could be compromised). I don't
see such a hard reason here.

Außerdem ist zu beachten, dass ein beschreibbares __definition_order__ dynamisch erstellten Klassen (z. B. von Cython) immer noch ermöglicht, __definition_order__ korrekt gesetzt zu haben. Dies könnte sicherlich durch spezifische Werkzeuge zur Klassenerstellung, wie type() oder die C-API, gehandhabt werden, ohne die Semantik eines schreibgeschützten Attributs zu verlieren. Mit einem beschreibbaren Attribut ist dies jedoch ein nicht relevantes Problem.

Warum nicht „__attribute_order__“?

__definition_order__ konzentriert sich auf den Klassendefinitionskörper. Die Anwendungsfälle für die Behandlung des Klassen-Namespaces (__dict__) nach der Definition sind eine separate Angelegenheit. __definition_order__ wäre ein stark irreführender Name für eine Funktion, die sich auf mehr als die Klassendefinition konzentriert.

Warum „dunder“-Namen ignorieren?

Namen, die mit „__“ beginnen und enden, sind für die Verwendung durch den Interpreter reserviert. In der Praxis sollten sie für die Benutzer von __definition_order__ nicht relevant sein. Stattdessen wären sie für fast jeden nur Ballast und verursachen die gleiche zusätzliche Arbeit (Herausfiltern der Dunder-Namen) für die Mehrheit. In Fällen, in denen ein Dunder-Name von Bedeutung ist, *könnte* die Klassendefinition __definition_order__ manuell setzen, was den häufigsten Fall vereinfacht.

Das Weglassen von Dunder-Namen aus __definition_order__ bedeutet jedoch, dass ihr Platz in der Definitionsreihenfolge unwiederbringlich verloren ginge. Das Weglassen von Dunder-Namen nach Standard kann unbeabsichtigt Probleme für Klassen verursachen, die Dunder-Namen unkonventionell verwenden. In diesem Fall ist es besser, auf Nummer sicher zu gehen und *alle* Namen aus der Klassendefinition zu übernehmen. Dies ist kein großes Problem, da es einfach ist, Dunder-Namen herauszufiltern.

(name for name in cls.__definition_order__
      if not (name.startswith('__') and name.endswith('__')))

Tatsächlich gibt es in einigen Anwendungskontexten möglicherweise andere Kriterien, nach denen ähnliche Filterungen angewendet würden, z. B. das Ignorieren aller Namen, die mit „_“ beginnen, das Weglassen aller Methoden oder das Einschließen nur von Deskriptoren. Letztendlich sind Dunder-Namen kein besonderer Fall, der ausnahmsweise behandelt werden müsste.

Beachten Sie, dass einige Dunder-Namen (__name__ und __qualname__) standardmäßig vom Compiler eingefügt werden. Sie werden also enthalten sein, obwohl sie nicht streng Teil des Klassendefinitionskörpers sind.

Warum None statt eines leeren Tupels?

Ein wichtiges Ziel der Hinzufügung von __definition_order__ ist die Beibehaltung von Informationen in Klassendefinitionen, die vor dieser PEP verloren gingen. Eine Konsequenz ist, dass __definition_order__ eine ursprüngliche Klassendefinition impliziert. Die Verwendung von None ermöglicht es uns, Klassen, die keine Definitionsreihenfolge haben, klar zu unterscheiden. Ein leeres Tupel zeigt eindeutig eine Klasse an, die aus einer Definitionsanweisung stammt, aber dort keine Attribute definiert hat.

Warum None statt des Nichtsetzens des Attributs?

Das Fehlen eines Attributs erfordert eine komplexere Handhabung als None für Konsumenten von __definition_order__.

Warum manuell gesetzte Werte einschränken?

Wenn __definition_order__ im Klassenkörper manuell gesetzt wird, wird es verwendet. Wir verlangen, dass es ein Tupel von Bezeichnern (oder None) ist, damit Konsumenten von __definition_order__ eine konsistente Erwartung für den Wert haben können. Dies trägt zur Maximierung des Nutzens der Funktion bei.

Wir könnten auch ein beliebiges iterierbares Objekt für ein manuell gesetztes __definition_order__ zulassen und es in ein Tupel umwandeln. Allerdings leiten nicht alle iterierbaren Objekte eine Definitionsreihenfolge ab (z. B. set). Daher entscheiden wir uns dafür, ein Tupel zu verlangen.

Warum __definition_order__ bei Nicht-Typ-Objekten ausblenden?

Python unternimmt kaum Anstrengungen, klassenspezifische Attribute während der Suche auf Instanzen von Klassen zu verbergen. Obwohl es sinnvoll sein mag, __definition_order__ als ein Klassen-spezifisches Attribut zu betrachten, das während der Suche auf Objekten verborgen ist, geht die Festlegung eines Präzedenzfalls in dieser Hinsicht über die Ziele dieser PEP hinaus.

Was ist mit __slots__?

__slots__ wird wie jeder andere Name im Klassendefinitionskörper zu __definition_order__ hinzugefügt. Die eigentlichen Slot-Namen werden __definition_order__ nicht hinzugefügt, da sie nicht als Namen im Definitions-Namespace gesetzt werden.

Warum ist __definition_order__ überhaupt notwendig?

Da die Definitionsreihenfolge in __dict__ nicht beibehalten wird, geht sie verloren, sobald die Ausführung der Klassendefinition abgeschlossen ist. Klassen *könnten* das Attribut als letztes im Körper explizit setzen. Dann könnten unabhängige Dekoratoren jedoch nur Klassen verwenden, die dies getan haben. Stattdessen bewahrt __definition_order__ dieses eine Informationsstück aus dem Klassenkörper, damit es universell verfügbar ist.

Unterstützung für C-API-Typen

Wohl die meisten C-definierten Python-Typen (z. B. eingebaute Typen, Erweiterungsmodule) haben ein annähernd äquivalentes Konzept einer Definitionsreihenfolge. Daher könnte __definition_order__ konzeptionell automatisch für solche Typen gesetzt werden. Diese PEP führt keine solche Unterstützung ein. Sie verbietet sie jedoch auch nicht. Da __definition_order__ jedoch jederzeit durch normale Attributzuweisung gesetzt werden kann, benötigt es in der C-API keine spezielle Behandlung.

Die spezifischen Fälle

  • eingebaute Typen
  • PyType_Ready
  • PyType_FromSpec

Kompatibilität

Diese PEP bricht die Abwärtskompatibilität nicht, außer in dem Fall, dass jemand *strikt* auf dict als Klassendefinitions-Namespace angewiesen ist. Dies sollte kein Problem sein, da issubclass(OrderedDict, dict) wahr ist.

Änderungen

Zusätzlich zur Klassensyntax geben die folgenden Elemente das neue Verhalten preis

  • builtins.__build_class__
  • types.prepare_class
  • types.new_class

Außerdem ermöglicht die 3-Argument-Form von builtins.type() die Aufnahme von __definition_order__ in den übergebenen Namespace. Er unterliegt denselben Beschränkungen wie beim expliziten Definieren von __definition_order__ im Klassenkörper.

Andere Python-Implementierungen

Vorbehaltlich des Feedbacks wird die Auswirkung auf Python-Implementierungen voraussichtlich minimal sein. Alle konformen Implementierungen werden voraussichtlich __definition_order__ wie in dieser PEP beschrieben setzen.

Implementierung

Die Implementierung ist im Tracker zu finden: https://github.com/python/cpython/issues/68442

Alternativen

Ein Reihenfolge-erhaltendes cls.__dict__

Anstatt die Definitionsreihenfolge in __definition_order__ zu speichern, könnte der nun geordnete Definitions-Namespace in ein neues OrderedDict kopiert werden. Dieses würde dann als die Zuordnung verwendet, die als __dict__ proxied wird. Dies würde größtenteils dieselbe Semantik liefern.

Die Verwendung von OrderedDict für __dict__ würde jedoch die Beziehung zum Definitions-Namespace verschleiern und ihn weniger nützlich machen.

Zusätzlich (im Fall von OrderedDict spezifisch) würde dies erhebliche Änderungen an der Semantik der konkreten dict C-API erfordern.

Es gab Diskussionen über den Übergang zu einer kompakten Dict-Implementierung, die (größtenteils) die Einfügungsreihenfolge beibehält. Das Fehlen eines expliziten __definition_order__ würde jedoch weiterhin ein Problem darstellen.

Ein „namespace“-Schlüsselwortargument für Klassendefinitionen

PEP 422 führte ein neues Schlüsselwortargument „namespace“ für Klassendefinitionen ein, das die Notwendigkeit von __prepare__() effektiv ersetzt. Der Vorschlag wurde jedoch zugunsten der einfacheren PEP 487 zurückgezogen.

Eine Metaklasse in der Standardbibliothek, die __prepare__() mit OrderedDict implementiert

Dies hat all die gleichen Probleme wie das Schreiben der eigenen Metaklasse. Der einzige Vorteil ist, dass man diese Metaklasse nicht tatsächlich schreiben muss. Daher bietet sie im Kontext dieser PEP keinen Vorteil.

Setzen von __definition_order__ zur Kompilierzeit

Der __qualname__ jeder Klasse wird zur Kompilierzeit bestimmt. Dieses gleiche Konzept könnte auf __definition_order__ angewendet werden. Das Ergebnis der Zusammensetzung von __definition_order__ zur Kompilierzeit wäre fast dasselbe wie zur Laufzeit.

Abgesehen von der Schwierigkeit der Implementierung wäre der Hauptunterschied, dass es zur Kompilierzeit nicht praktikabel wäre, die Definitionsreihenfolge für Attribute beizubehalten, die dynamisch im Klassenkörper gesetzt werden (z. B. locals()[name] = value). Sie sollten jedoch immer noch in der Definitionsreihenfolge berücksichtigt werden. Eine mögliche Lösung wäre, Klassenautoren zu zwingen, __definition_order__ manuell zu setzen, wenn sie dynamisch Klassenattribute definieren.

Letztendlich ist die Verwendung von OrderedDict zur Laufzeit oder die Entdeckung zur Kompilierzeit fast ausschließlich eine Implementierungsfrage.

Referenzen


Quelle: https://github.com/python/peps/blob/main/peps/pep-0520.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT