PEP 557 – Data Klassen
- Autor:
- Eric V. Smith <eric at trueblade.com>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 02. Juni 2017
- Python-Version:
- 3.7
- Post-History:
- 08. Sep. 2017, 25. Nov. 2017, 30. Nov. 2017, 01. Dez. 2017, 02. Dez. 2017, 06. Jan. 2018, 04. Mär. 2018
- Resolution:
- Python-Dev Nachricht
Hinweis für Gutachter
Dieses PEP und die anfängliche Implementierung wurden in einem separaten Repository erstellt: https://github.com/ericvsmith/dataclasses. Bevor Sie in einem öffentlichen Forum kommentieren, lesen Sie bitte zumindest die Diskussion am Ende dieses PEP.
Zusammenfassung
Dieses PEP beschreibt eine Ergänzung zur Standardbibliothek namens Data Classes. Obwohl sie einen sehr unterschiedlichen Mechanismus verwenden, können Data Classes als "veränderliche namedtuples mit Standardwerten" betrachtet werden. Da Data Classes die normale Klassendefinitionssyntax verwenden, können Sie Vererbung, Metaklassen, Docstrings, benutzerdefinierte Methoden, Klassenfabriken und andere Python-Klassenfunktionen frei nutzen.
Ein Klassen-Decorator wird bereitgestellt, der eine Klassendefinition auf Variablen mit Typannotationen gemäß PEP 526, "Syntax for Variable Annotations", untersucht. In diesem Dokument werden solche Variablen als Felder bezeichnet. Mithilfe dieser Felder fügt der Decorator generierte Methodendefinitionen zur Klasse hinzu, um Instanzinitialisierung, ein repr, Vergleichsmethoden und optional andere Methoden gemäß dem Abschnitt Spezifikation zu unterstützen. Eine solche Klasse wird als Data Class bezeichnet, aber es ist nichts Besonderes an der Klasse: Der Decorator fügt der Klasse generierte Methoden hinzu und gibt die gleiche Klasse zurück, die er erhalten hat.
Als Beispiel:
@dataclass
class InventoryItem:
'''Class for keeping track of an item in inventory.'''
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
Der @dataclass-Decorator fügt der Klasse InventoryItem die folgenden Methoden hinzu
def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0) -> None:
self.name = name
self.unit_price = unit_price
self.quantity_on_hand = quantity_on_hand
def __repr__(self):
return f'InventoryItem(name={self.name!r}, unit_price={self.unit_price!r}, quantity_on_hand={self.quantity_on_hand!r})'
def __eq__(self, other):
if other.__class__ is self.__class__:
return (self.name, self.unit_price, self.quantity_on_hand) == (other.name, other.unit_price, other.quantity_on_hand)
return NotImplemented
def __ne__(self, other):
if other.__class__ is self.__class__:
return (self.name, self.unit_price, self.quantity_on_hand) != (other.name, other.unit_price, other.quantity_on_hand)
return NotImplemented
def __lt__(self, other):
if other.__class__ is self.__class__:
return (self.name, self.unit_price, self.quantity_on_hand) < (other.name, other.unit_price, other.quantity_on_hand)
return NotImplemented
def __le__(self, other):
if other.__class__ is self.__class__:
return (self.name, self.unit_price, self.quantity_on_hand) <= (other.name, other.unit_price, other.quantity_on_hand)
return NotImplemented
def __gt__(self, other):
if other.__class__ is self.__class__:
return (self.name, self.unit_price, self.quantity_on_hand) > (other.name, other.unit_price, other.quantity_on_hand)
return NotImplemented
def __ge__(self, other):
if other.__class__ is self.__class__:
return (self.name, self.unit_price, self.quantity_on_hand) >= (other.name, other.unit_price, other.quantity_on_hand)
return NotImplemented
Data Klassen ersparen Ihnen das Schreiben und Pflegen dieser Methoden.
Begründung
Es gab zahlreiche Versuche, Klassen zu definieren, die hauptsächlich dazu dienen, Werte zu speichern, die durch Attributabruf zugänglich sind. Einige Beispiele sind
- collections.namedtuple in der Standardbibliothek.
- typing.NamedTuple in der Standardbibliothek.
- Das beliebte [1] Projekt attrs.
- George Sakkis' recordType-Rezept [2], ein veränderlicher Datentyp, inspiriert von collections.namedtuple.
- Viele Online-Rezepte [3], Pakete [4] und Fragen [5]. David Beazley verwendete eine Form von Data Classes als motivierendes Beispiel in einem Vortrag über Metaklassen auf der PyCon 2013 [6].
Warum wird dieses PEP benötigt?
Mit der Einführung von PEP 526 verfügt Python über eine prägnante Möglichkeit, den Typ von Klassenmitgliedern zu spezifizieren. Dieses PEP nutzt diese Syntax, um eine einfache, unaufdringliche Methode zur Beschreibung von Data Classes bereitzustellen. Mit zwei Ausnahmen wird die angegebene Typannotation des Attributs von Data Classes vollständig ignoriert.
Es werden keine Basisklassen oder Metaklassen von Data Classes verwendet. Benutzer dieser Klassen können Vererbung und Metaklassen frei und ohne Störung durch Data Classes verwenden. Die dekorierten Klassen sind wirklich "normale" Python-Klassen. Der Data Class Decorator sollte die Nutzung der Klasse nicht beeinträchtigen.
Ein Hauptentwicklungsziel von Data Classes ist die Unterstützung von statischen Typüberprüfern. Die Verwendung der PEP 526-Syntax ist ein Beispiel hierfür, ebenso wie das Design der Funktion fields() und des Decorators @dataclass. Aufgrund ihrer sehr dynamischen Natur sind einige der oben genannten Bibliotheken schwierig mit statischen Typüberprüfern zu verwenden.
Data Classes sind kein Ersatzmechanismus für alle oben genannten Bibliotheken und sind auch nicht dazu gedacht, dies zu sein. Aber die Aufnahme in die Standardbibliothek ermöglicht es vielen einfacheren Anwendungsfällen, stattdessen Data Classes zu nutzen. Viele der aufgeführten Bibliotheken haben unterschiedliche Funktionsumfänge und werden natürlich weiter existieren und gedeihen.
Wo ist die Verwendung von Data Classes ungeeignet?
- API-Kompatibilität mit Tupeln oder Dictionaries ist erforderlich.
- Typvalidierung über das hinaus, was durch PEP 484 und 526 bereitgestellt wird, ist erforderlich, oder Wertvalidierung oder -konvertierung ist erforderlich.
Spezifikation
Alle in diesem PEP beschriebenen Funktionen werden in einem Modul namens dataclasses untergebracht.
Eine Funktion dataclass, die typischerweise als Klassen-Decorator verwendet wird, wird bereitgestellt, um Klassen nachzubearbeiten und generierte Methoden hinzuzufügen, wie nachstehend beschrieben.
Der dataclass-Decorator untersucht die Klasse, um fields zu finden. Ein field ist definiert als jede Variable, die in __annotations__ identifiziert wird. Das heißt, eine Variable, die eine Typannotation hat. Mit zwei Ausnahmen, die unten beschrieben werden, wird keine der Data Class-Mechanismen den in der Annotation angegebenen Typ untersuchen.
Beachten Sie, dass __annotations__ garantiert eine geordnete Zuordnung in der Reihenfolge der Klassendefinition ist. Die Reihenfolge der Felder in allen generierten Methoden ist die Reihenfolge, in der sie in der Klasse erscheinen.
Der dataclass-Decorator fügt der Klasse verschiedene "Dunder"-Methoden hinzu, die nachstehend beschrieben werden. Wenn eine der hinzugefügten Methoden bereits in der Klasse vorhanden ist, wird ein TypeError ausgelöst. Der Decorator gibt dieselbe Klasse zurück, die aufgerufen wird: Es wird keine neue Klasse erstellt.
Der dataclass-Decorator wird typischerweise ohne Parameter und Klammern verwendet. Er unterstützt jedoch auch die folgende logische Signatur
def dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
Wenn dataclass nur als einfacher Decorator ohne Parameter verwendet wird, verhält er sich so, als ob er die in dieser Signatur dokumentierten Standardwerte hätte. Das heißt, diese drei Verwendungen von @dataclass sind äquivalent
@dataclass
class C:
...
@dataclass()
class C:
...
@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
...
Die Parameter für dataclass sind
init: Wenn wahr (Standard), wird eine__init__-Methode generiert.repr: Wenn wahr (Standard), wird eine__repr__-Methode generiert. Der generierte repr-String enthält den Klassennamen und den Namen und repr jedes Feldes in der Reihenfolge ihrer Definition in der Klasse. Felder, die als vom repr ausgeschlossen markiert sind, werden nicht enthalten. Zum Beispiel:InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10).Wenn die Klasse bereits
__repr__definiert, wird dieser Parameter ignoriert.eq: Wenn wahr (Standard), wird eine__eq__-Methode generiert. Diese Methode vergleicht die Klasse, als wäre sie ein Tupel ihrer Felder, in Reihenfolge. Beide Instanzen im Vergleich müssen vom exakt gleichen Typ sein.Wenn die Klasse bereits
__eq__definiert, wird dieser Parameter ignoriert.order: Wenn wahr (Standard ist False), werden die Methoden__lt__,__le__,__gt__und__ge__generiert. Diese vergleichen die Klasse, als wäre sie ein Tupel ihrer Felder, in Reihenfolge. Beide Instanzen im Vergleich müssen vom exakt gleichen Typ sein. Wennorderwahr ist undeqfalsch ist, wird einValueErrorausgelöst.Wenn die Klasse bereits eine der Methoden
__lt__,__le__,__gt__oder__ge__definiert, wird einValueErrorausgelöst.unsafe_hash: WennFalse(Standard), wird die Methode__hash__gemäß den Einstellungen vonequndfrozengeneriert.Wenn
equndfrozenbeide wahr sind, generiert Data Classes eine__hash__-Methode für Sie. Wenneqwahr ist undfrozenfalsch ist, wird__hash__aufNonegesetzt, was sie als nicht hashbar markiert (was sie ist). Wenneqfalsch ist, bleibt__hash__unverändert, was bedeutet, dass die__hash__-Methode der Superklasse verwendet wird (wenn die Superklasseobjectist, bedeutet dies, dass sie auf id-basierte Hashing zurückfällt).Obwohl nicht empfohlen, können Sie Data Classes zwingen, eine
__hash__-Methode mitunsafe_hash=Truezu erstellen. Dies kann der Fall sein, wenn Ihre Klasse logisch unveränderlich ist, aber dennoch verändert werden kann. Dies ist ein spezialisierter Anwendungsfall und sollte sorgfältig bedacht werden.Wenn eine Klasse bereits ein explizit definiertes
__hash__hat, wird das Verhalten beim Hinzufügen von__hash__modifiziert. Ein explizit definiertes__hash__ist definiert, wenn__eq__in der Klasse definiert ist und__hash__mit einem anderen Wert alsNonedefiniert ist.__eq__in der Klasse definiert ist und ein__hash__ungleichNonedefiniert ist.__eq__nicht in der Klasse definiert ist und ein__hash__definiert ist.
Wenn
unsafe_hashwahr ist und ein explizit definiertes__hash__vorhanden ist, wird einValueErrorausgelöst.Wenn
unsafe_hashfalsch ist und ein explizit definiertes__hash__vorhanden ist, wird keine__hash__hinzugefügt.Weitere Informationen finden Sie in der Python-Dokumentation [7].
frozen: Wenn wahr (Standard ist False), generiert die Zuweisung an Felder eine Ausnahme. Dies emuliert schreibgeschützte eingefrorene Instanzen. Wenn entweder__getattr__oder__setattr__in der Klasse definiert ist, wird einValueErrorausgelöst. Siehe die nachstehende Diskussion.
fields können optional einen Standardwert mit normaler Python-Syntax angeben
@dataclass
class C:
a: int # 'a' has no default value
b: int = 0 # assign a default value for 'b'
In diesem Beispiel werden sowohl a als auch b in der hinzugefügten __init__-Methode enthalten sein, die wie folgt definiert wird
def __init__(self, a: int, b: int = 0):
Wenn ein Feld ohne Standardwert einem Feld mit Standardwert folgt, wird ein TypeError ausgelöst. Dies gilt sowohl, wenn dies in einer einzelnen Klasse auftritt, als auch infolge von Klassenerbschaft.
Für gängige und einfache Anwendungsfälle ist keine weitere Funktionalität erforderlich. Es gibt jedoch einige Data Class-Funktionen, die zusätzliche Informationen pro Feld erfordern. Um diesen Bedarf an zusätzlichen Informationen zu decken, können Sie den Standard-Feldwert durch einen Aufruf der bereitgestellten Funktion field() ersetzen. Die Signatur von field() ist
def field(*, default=MISSING, default_factory=MISSING, repr=True,
hash=None, init=True, compare=True, metadata=None)
Der MISSING-Wert ist ein Sentinel-Objekt, das verwendet wird, um zu erkennen, ob die Parameter default und default_factory angegeben sind. Dieses Sentinel wird verwendet, da None ein gültiger Wert für default ist.
Die Parameter für field() sind
default: Wenn angegeben, ist dies der Standardwert für dieses Feld. Dies ist notwendig, da derfield-Aufruf selbst die normale Position des Standardwerts ersetzt.default_factory: Wenn angegeben, muss dies ein Null-Argument-Callable sein, das aufgerufen wird, wenn ein Standardwert für dieses Feld benötigt wird. Dies kann unter anderem verwendet werden, um Felder mit veränderlichen Standardwerten anzugeben, wie nachstehend diskutiert. Es ist ein Fehler, sowohldefaultals auchdefault_factoryanzugeben.init: Wenn wahr (Standard), wird dieses Feld als Parameter für die generierte__init__-Methode aufgenommen.repr: Wenn wahr (Standard), wird dieses Feld in den von der generierten__repr__-Methode zurückgegebenen String aufgenommen.compare: Wenn wahr (Standard), wird dieses Feld in die generierten Gleichheits- und Vergleichsmethoden (__eq__,__gt__, etc.) aufgenommen.hash: Dies kann ein Boolescher Wert oderNonesein. Wenn wahr, wird dieses Feld in die generierte__hash__-Methode aufgenommen. WennNone(Standard), wird der Wert voncompareverwendet: Dies wäre normalerweise das erwartete Verhalten. Ein Feld sollte in den Hash einbezogen werden, wenn es für Vergleiche verwendet wird. Das Setzen dieses Wertes auf etwas anderes alsNonewird nicht empfohlen.Ein möglicher Grund für
hash=False, abercompare=Truewäre, wenn ein Feld teuer zu berechnen für einen Hashwert ist, dieses Feld für den Gleichheitstest benötigt wird und es andere Felder gibt, die zum Hashwert des Typs beitragen. Selbst wenn ein Feld vom Hash ausgeschlossen ist, wird es immer noch für Vergleiche verwendet.metadata: Dies kann eine Zuordnung oder None sein. None wird als leeres Dict behandelt. Dieser Wert wird intypes.MappingProxyTypeeingepackt, um ihn schreibgeschützt zu machen, und auf dem Field-Objekt exponiert. Er wird von Data Classes überhaupt nicht verwendet und dient als Erweiterungsmechanismus für Dritte. Mehrere Dritte können ihren eigenen Schlüssel haben, der als Namespace in den Metadaten verwendet wird.
Wenn der Standardwert eines Feldes durch einen Aufruf von field() angegeben wird, wird das Klassenattribut für dieses Feld durch den angegebenen default-Wert ersetzt. Wenn kein default angegeben ist, wird das Klassenattribut gelöscht. Die Absicht ist, dass nach Ausführung des dataclass-Decorators alle Klassenattribute die Standardwerte für die Felder enthalten, so als ob der Standardwert selbst angegeben worden wäre. Zum Beispiel, nach
@dataclass
class C:
x: int
y: int = field(repr=False)
z: int = field(repr=False, default=10)
t: int = 20
Das Klassenattribut C.z wird 10 sein, das Klassenattribut C.t wird 20 sein, und die Klassenattribute C.x und C.y werden nicht gesetzt.
Field-Objekte
Field-Objekte beschreiben jedes definierte Feld. Diese Objekte werden intern erstellt und von der Modul-weiten Methode fields() (siehe unten) zurückgegeben. Benutzer sollten niemals direkt ein Field-Objekt instanziieren. Seine dokumentierten Attribute sind
name: Der Name des Feldes.type: Der Typ des Feldes.default,default_factory,init,repr,hash,compareundmetadatahaben die identische Bedeutung und Werte wie in der Deklarationfield().
Andere Attribute können existieren, aber sie sind privat und dürfen nicht inspiziert oder darauf aufgebaut werden.
Post-init-Verarbeitung
Der generierte Code für __init__ ruft eine Methode namens __post_init__ auf, falls diese in der Klasse definiert ist. Sie wird als self.__post_init__() aufgerufen. Wenn keine __init__-Methode generiert wird, wird __post_init__ nicht automatisch aufgerufen.
Dies ermöglicht unter anderem die Initialisierung von Feldwerten, die von einem oder mehreren anderen Feldern abhängen. Zum Beispiel
@dataclass
class C:
a: float
b: float
c: float = field(init=False)
def __post_init__(self):
self.c = self.a + self.b
Siehe den Abschnitt unten über Nur-Init-Variablen für Möglichkeiten, Parameter an __post_init__() zu übergeben. Siehe auch die Warnung, wie replace() init=False-Felder behandelt.
Klassenvariablen
Ein Bereich, in dem dataclass tatsächlich den Typ eines Feldes untersucht, ist die Bestimmung, ob ein Feld eine Klassenvariable gemäß PEP 526 ist. Dies geschieht durch Überprüfung, ob der Typ des Feldes typing.ClassVar ist. Wenn ein Feld eine ClassVar ist, wird es von der Betrachtung als Feld ausgeschlossen und von den Data Class-Mechanismen ignoriert. Weitere Diskussionen finden Sie unter [8]. Solche ClassVar-Pseudo-Felder werden von der Modul-weiten Funktion fields() nicht zurückgegeben.
Nur-Init-Variablen
Der andere Bereich, in dem dataclass eine Typannotation untersucht, ist die Bestimmung, ob ein Feld eine Nur-Init-Variable ist. Dies geschieht, indem geprüft wird, ob der Typ eines Feldes vom Typ dataclasses.InitVar ist. Wenn ein Feld eine InitVar ist, wird es als Pseudo-Feld namens Init-Only Field betrachtet. Da es sich nicht um ein echtes Feld handelt, wird es von der Modul-weiten Funktion fields() nicht zurückgegeben. Init-Only Felder werden als Parameter für die generierte __init__-Methode hinzugefügt und an die optionale __post_init__-Methode übergeben. Sie werden von Data Classes ansonsten nicht verwendet.
Nehmen wir an, ein Feld wird aus einer Datenbank initialisiert, wenn bei der Erstellung der Klasse kein Wert angegeben wird
@dataclass
class C:
i: int
j: int = None
database: InitVar[DatabaseType] = None
def __post_init__(self, database):
if self.j is None and database is not None:
self.j = database.lookup('j')
c = C(10, database=my_database)
In diesem Fall gibt fields() Field-Objekte für i und j zurück, aber nicht für database.
Eingefrorene Instanzen
Es ist nicht möglich, echte unveränderliche Python-Objekte zu erstellen. Durch die Übergabe von frozen=True an den Decorator @dataclass können Sie jedoch Unveränderlichkeit emulieren. In diesem Fall fügt Data Classes die Methoden __setattr__ und __delattr__ zur Klasse hinzu. Diese Methoden lösen eine FrozenInstanceError aus, wenn sie aufgerufen werden.
Bei der Verwendung von frozen=True gibt es einen geringen Performance-Nachteil: __init__ kann keine einfache Zuweisung zur Initialisierung von Feldern verwenden und muss object.__setattr__ verwenden.
Vererbung
Wenn die Data Class vom Decorator @dataclass erstellt wird, durchsucht sie alle Basisklassen der Klasse in umgekehrter MRO-Reihenfolge (d.h. beginnend bei object) und fügt für jede gefundene Data Class die Felder dieser Basisklasse zu einer geordneten Zuordnung von Feldern hinzu. Nachdem alle Felder der Basisklasse hinzugefügt wurden, fügt sie ihre eigenen Felder zur geordneten Zuordnung hinzu. Alle generierten Methoden verwenden diese kombinierte, berechnete geordnete Zuordnung von Feldern. Da die Felder in der Reihenfolge ihrer Einfügung stehen, überschreiben abgeleitete Klassen Basisklassen. Ein Beispiel
@dataclass
class Base:
x: Any = 15.0
y: int = 0
@dataclass
class C(Base):
z: int = 10
x: int = 15
Die endgültige Liste der Felder ist in der Reihenfolge x, y, z. Der endgültige Typ von x ist int, wie in Klasse C angegeben.
Die generierte __init__-Methode für C sieht wie folgt aus
def __init__(self, x: int = 15, y: int = 0, z: int = 10):
Standard-Factory-Funktionen
Wenn ein Feld eine default_factory angibt, wird diese mit null Argumenten aufgerufen, wenn ein Standardwert für das Feld benötigt wird. Zum Beispiel, um eine neue Instanz einer Liste zu erstellen, verwenden Sie
l: list = field(default_factory=list)
Wenn ein Feld von __init__ ausgeschlossen wird (mit init=False) und das Feld auch eine default_factory angibt, dann wird die Default-Factory-Funktion immer aus der generierten __init__-Funktion aufgerufen. Dies geschieht, da es keine andere Möglichkeit gibt, dem Feld einen Anfangswert zu geben.
Veränderliche Standardwerte
Python speichert Standardwerte für Membervariablen in Klassenattributen. Betrachten Sie dieses Beispiel ohne Data Classes
class C:
x = []
def add(self, element):
self.x += element
o1 = C()
o2 = C()
o1.add(1)
o2.add(2)
assert o1.x == [1, 2]
assert o1.x is o2.x
Beachten Sie, dass die beiden Instanzen der Klasse C dieselbe Klassenvariable x teilen, wie erwartet.
Mit Data Classes, *wenn* dieser Code gültig wäre
@dataclass
class D:
x: List = []
def add(self, element):
self.x += element
würde er Code ähnlich wie diesen generieren
class D:
x = []
def __init__(self, x=x):
self.x = x
def add(self, element):
self.x += element
assert D().x is D().x
Dies hat dasselbe Problem wie das ursprüngliche Beispiel mit Klasse C. Das heißt, zwei Instanzen der Klasse D, die keinen Wert für x bei der Erstellung einer Klasseninstanz angeben, teilen sich dieselbe Kopie von x. Da Data Classes normale Python-Klassenerstellung verwenden, teilen sie sich auch dieses Problem. Es gibt keine allgemeine Möglichkeit für Data Classes, diesen Zustand zu erkennen. Stattdessen löst Data Classes einen TypeError aus, wenn sie einen Standardparameter vom Typ list, dict oder set erkennt. Dies ist eine partielle Lösung, schützt aber vor vielen gängigen Fehlern. Weitere Einzelheiten finden Sie unter Automatische Unterstützung für veränderliche Standardwerte im Abschnitt Abgelehnte Ideen.
Die Verwendung von Default-Factory-Funktionen ist eine Möglichkeit, neue Instanzen von veränderlichen Typen als Standardwerte für Felder zu erstellen
@dataclass
class D:
x: list = field(default_factory=list)
assert D().x is not D().x
Hilfsfunktionen auf Modulebene
fields(class_or_instance): Gibt ein Tupel vonField-Objekten zurück, die die Felder für diese Data Class definieren. Akzeptiert entweder eine Data Class oder eine Instanz einer Data Class. LöstValueErroraus, wenn keine Data Class oder eine Instanz davon übergeben wird. Gibt keine Pseudo-Felder zurück, dieClassVaroderInitVarsind.asdict(instance, *, dict_factory=dict): Konvertiert die Data Classinstancein ein Dict (unter Verwendung der Factory-Funktiondict_factory). Jede Data Class wird in ein Dict ihrer Felder konvertiert, als Name:Wert-Paare. Data Classes, Dicts, Listen und Tupel werden rekursiv durchlaufen. Zum Beispiel@dataclass class Point: x: int y: int @dataclass class C: l: List[Point] p = Point(10, 20) assert asdict(p) == {'x': 10, 'y': 20} c = C([Point(0, 0), Point(10, 4)]) assert asdict(c) == {'l': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
Löst
TypeErroraus, wenninstancekeine Data Class-Instanz ist.astuple(*, tuple_factory=tuple): Konvertiert die Data Classinstancein ein Tupel (unter Verwendung der Factory-Funktiontuple_factory). Jede Data Class wird in ein Tupel ihrer Feldwerte konvertiert. Data Classes, Dicts, Listen und Tupel werden rekursiv durchlaufen.Fortsetzung des vorherigen Beispiels
assert astuple(p) == (10, 20) assert astuple(c) == ([(0, 0), (10, 4)],)
Löst
TypeErroraus, wenninstancekeine Data Class-Instanz ist.make_dataclass(cls_name, fields, *, bases=(), namespace=None): Erstellt eine neue Data Class mit dem Namencls_name, Feldern wie infieldsdefiniert, Basisklassen wie inbasesangegeben und initialisiert mit einem Namespace wie innamespaceangegeben.fieldsist ein iterierbares Element, dessen Elemente entwedername,(name, type)oder(name, type, Field)sind. Wenn nurnameangegeben wird, wirdtyping.Anyfürtypeverwendet. Diese Funktion ist nicht unbedingt erforderlich, da jeder Python-Mechanismus zur Erstellung einer neuen Klasse mit__annotations__dann die Funktiondataclassanwenden kann, um diese Klasse in eine Data Class zu konvertieren. Diese Funktion wird als Bequemlichkeit angeboten. Zum BeispielC = make_dataclass('C', [('x', int), 'y', ('z', int, field(default=5))], namespace={'add_one': lambda self: self.x + 1})
Entspricht
@dataclass class C: x: int y: 'typing.Any' z: int = 5 def add_one(self): return self.x + 1
replace(instance, **changes): Erstellt ein neues Objekt desselben Typs wieinstanceund ersetzt Felder durch Werte auschanges. Wenninstancekeine Data Class ist, wirdTypeErrorausgelöst. Wenn Werte inchangeskeine Felder angeben, wirdTypeErrorausgelöst.Das neu zurückgegebene Objekt wird durch Aufruf der Methode
__init__der Data Class erstellt. Dies stellt sicher, dass__post_init__, falls vorhanden, ebenfalls aufgerufen wird.Nur für die Initialisierung vorgesehene Variablen ohne Standardwerte, falls vorhanden, müssen beim Aufruf von
replaceangegeben werden, damit sie an__init__und__post_init__übergeben werden können.Es ist ein Fehler, wenn
changesFelder enthält, die alsinit=Falsedefiniert sind. In diesem Fall wird einValueErrorausgelöst.Seien Sie sich bewusst, wie
init=FalseFelder bei einem Aufruf vonreplace()funktionieren. Sie werden nicht vom Quellobjekt kopiert, sondern in__post_init__()initialisiert, falls sie überhaupt initialisiert werden. Es wird erwartet, dassinit=FalseFelder selten und sparsam verwendet werden. Wenn sie verwendet werden, ist es ratsam, alternative Klassenkonstruktoren zu haben, oder vielleicht eine benutzerdefinierte Methodereplace()(oder ähnlich benannt), die das Kopieren von Instanzen behandelt.is_dataclass(class_or_instance): Gibt True zurück, wenn der Parameter eine Data Class oder eine Instanz davon ist, andernfalls False.Wenn Sie wissen müssen, ob eine Klasse eine Instanz einer Data Class ist (und nicht selbst eine Data Class), fügen Sie eine weitere Prüfung für
not isinstance(obj, type)hinzu.def is_dataclass_instance(obj): return is_dataclass(obj) and not isinstance(obj, type)
Diskussion
python-ideas Diskussion
Diese Diskussion begann auf python-ideas [9] und wurde in ein GitHub-Repository [10] zur weiteren Diskussion verschoben. Im Rahmen dieser Diskussion haben wir die Entscheidung getroffen, die Syntax von PEP 526 für die Erkennung von Feldern zu verwenden.
Unterstützung für die automatische Einstellung von __slots__?
Zumindest für die Erstveröffentlichung wird __slots__ nicht unterstützt. __slots__ muss zur Zeit der Klassenerstellung hinzugefügt werden. Der Data Class-Decorator wird nach der Klassenerstellung aufgerufen. Um __slots__ hinzuzufügen, müsste der Decorator eine neue Klasse erstellen, __slots__ setzen und sie zurückgeben. Da dieses Verhalten etwas überraschend ist, unterstützt die Anfangsversion von Data Classes das automatische Setzen von __slots__ nicht. Es gibt eine Reihe von Workarounds
- Fügen Sie
__slots__manuell in die Klassendefinition ein. - Schreiben Sie eine Funktion (die als Decorator verwendet werden könnte), die die Klasse mit
fields()inspiziert und eine neue Klasse mit gesetzten__slots__erstellt.
Für weitere Diskussionen siehe [11].
Warum nicht einfach namedtuple verwenden?
- Jedes benannte Tupel kann versehentlich mit jedem anderen mit derselben Anzahl von Feldern verglichen werden. Zum Beispiel:
Point3D(2017, 6, 2) == Date(2017, 6, 2). Mit Data Classes würde dies False ergeben. - Ein benanntes Tupel kann versehentlich mit einem Tupel verglichen werden. Zum Beispiel:
Point2D(1, 10) == (1, 10). Mit Data Classes würde dies False ergeben. - Instanzen sind immer iterierbar, was das Hinzufügen von Feldern erschweren kann. Wenn eine Bibliothek definiert
Time = namedtuple('Time', ['hour', 'minute']) def get_time(): return Time(12, 0)
Dann wenn ein Benutzer diesen Code wie folgt verwendet
hour, minute = get_time()
dann wäre es nicht möglich, ein
second-Feld zuTimehinzuzufügen, ohne den Code des Benutzers zu brechen. - Keine Option für mutable Instanzen.
- Kann keine Standardwerte angeben.
- Kann nicht steuern, welche Felder für
__init__,__repr__usw. verwendet werden. - Kann die Kombination von Feldern durch Vererbung nicht unterstützen.
Warum nicht einfach typing.NamedTuple verwenden?
Für Klassen mit statisch definierten Feldern unterstützt es eine ähnliche Syntax wie Data Classes mit Typannotationen. Dies erzeugt ein benanntes Tupel, so dass es die Vorteile von namedtuple und einige seiner Nachteile teilt. Data Classes unterstützen im Gegensatz zu typing.NamedTuple die Kombination von Feldern durch Vererbung.
Warum nicht einfach attrs verwenden?
- attrs entwickelt sich schneller, als berücksichtigt werden könnte, wenn es in die Standardbibliothek aufgenommen würde.
- attrs unterstützt zusätzliche Funktionen, die hier nicht vorgeschlagen werden: Validatoren, Konverter, Metadaten usw. Data Classes treffen eine Entscheidung, um die Einfachheit zu wahren, indem sie diese Funktionen nicht implementieren.
Für weitere Diskussionen siehe [12].
Post-init-Parameter
In einer früheren Version dieser PEP, bevor InitVar hinzugefügt wurde, hat die Post-Init-Funktion __post_init__ keine Parameter erhalten.
Der normale Weg, parametrisierte Initialisierungen durchzuführen (und nicht nur mit Data Classes), ist die Bereitstellung eines alternativen Klassenmethoden-Konstruktors. Zum Beispiel
@dataclass
class C:
x: int
@classmethod
def from_file(cls, filename):
with open(filename) as fl:
file_value = int(fl.read())
return C(file_value)
c = C.from_file('file.txt')
Da die Funktion __post_init__ das Letzte ist, was im generierten __init__ aufgerufen wird, ist ein Klassenmethoden-Konstruktor (der auch Code unmittelbar nach der Objekterstellung ausführen kann) funktional äquivalent dazu, Parameter an eine __post_init__-Funktion übergeben zu können.
Mit InitVars können __post_init__-Funktionen nun Parameter erhalten. Sie werden zuerst an __init__ übergeben, das sie an __post_init__ weiterleitet, wo der Benutzer-Code sie nach Bedarf verwenden kann.
Der einzige wirkliche Unterschied zwischen alternativen Klassenmethoden-Konstruktoren und InitVar-Pseudo-Feldern liegt in Bezug auf erforderliche Nicht-Feld-Parameter während der Objekterstellung. Mit InitVars müssen bei der Verwendung von __init__ und der Modul-weiten Funktion replace() InitVars immer angegeben werden. Betrachten Sie den Fall, dass ein context-Objekt zur Erstellung einer Instanz benötigt wird, aber nicht als Feld gespeichert wird. Mit alternativen Klassenmethoden-Konstruktoren ist der context-Parameter immer optional, da Sie das Objekt immer noch erstellen könnten, indem Sie __init__ durchlaufen (es sei denn, Sie unterdrücken dessen Erstellung). Welcher Ansatz besser geeignet ist, hängt von der Anwendung ab, aber beide Ansätze werden unterstützt.
Ein weiterer Grund für die Verwendung von InitVar-Feldern ist, dass der Klassenautor die Reihenfolge der __init__-Parameter steuern kann. Dies ist besonders wichtig bei regulären Feldern und InitVar-Feldern mit Standardwerten, da alle Felder mit Standardwerten nach allen Feldern ohne Standardwerte kommen müssen. Ein früheres Design sah vor, dass alle Init-Only-Felder nach den regulären Feldern kamen. Das bedeutete, dass, wenn irgendein Feld einen Standardwert hatte, alle Init-Only-Felder ebenfalls Standardwerte haben mussten.
Namen der Funktionen asdict und astuple
Die Namen der Modul-weiten Hilfsfunktionen asdict() und astuple() sind wohl nicht PEP 8-konform und sollten as_dict() bzw. as_tuple() sein. Nach Diskussionen [13] wurde jedoch beschlossen, die Konsistenz mit namedtuple._asdict() und attr.asdict() beizubehalten.
Abgelehnte Ideen
Kopieren von init=False-Feldern nach der Erstellung eines neuen Objekts in replace()
Felder, die init=False sind, werden per Definition nicht an __init__ übergeben, sondern stattdessen mit einem Standardwert initialisiert, oder durch Aufruf einer Standard-Factory-Funktion in __init__, oder durch Code in __post_init__.
Eine frühere Version dieser PEP gab vor, dass init=False Felder vom Quellobjekt auf das neu erstellte Objekt kopiert würden, nachdem __init__ zurückgekehrt war, aber das wurde als inkonsistent mit der Verwendung von __init__ und __post_init__ zur Initialisierung des neuen Objekts angesehen. Zum Beispiel betrachten wir diesen Fall
@dataclass
class Square:
length: float
area: float = field(init=False, default=0.0)
def __post_init__(self):
self.area = self.length * self.length
s1 = Square(1.0)
s2 = replace(s1, length=2.0)
Wenn init=False Felder vom Quell- auf das Zielobjekt kopiert würden, nachdem __post_init__ ausgeführt wurde, dann wäre s2 Square(length=2.0, area=1.0), anstatt des korrekten Square(length=2.0, area=4.0).
Automatische Unterstützung für veränderliche Standardwerte
Ein Vorschlag war, Standardwerte automatisch zu kopieren, so dass, wenn eine literale Liste [] ein Standardwert wäre, jede Instanz eine neue Liste erhalten würde. Es gab unerwünschte Nebenwirkungen dieser Entscheidung, so dass die endgültige Entscheidung ist, die 3 bekannten eingebauten veränderlichen Typen zu verbieten: Liste, Dict und Set. Für eine vollständige Diskussion dieser und anderer Optionen siehe [14].
Beispiele
Benutzerdefinierte __init__-Methode
Manchmal reicht die generierte Methode __init__ nicht aus. Zum Beispiel, wenn Sie ein Objekt haben möchten, das *args und **kwargs speichert
@dataclass(init=False)
class ArgHolder:
args: List[Any]
kwargs: Mapping[Any, Any]
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
a = ArgHolder(1, 2, three=3)
Ein kompliziertes Beispiel
Dieser Code existiert in einem Closed-Source-Projekt
class Application:
def __init__(self, name, requirements, constraints=None, path='', executable_links=None, executables_dir=()):
self.name = name
self.requirements = requirements
self.constraints = {} if constraints is None else constraints
self.path = path
self.executable_links = [] if executable_links is None else executable_links
self.executables_dir = executables_dir
self.additional_items = []
def __repr__(self):
return f'Application({self.name!r},{self.requirements!r},{self.constraints!r},{self.path!r},{self.executable_links!r},{self.executables_dir!r},{self.additional_items!r})'
Dies kann ersetzt werden durch
@dataclass
class Application:
name: str
requirements: List[Requirement]
constraints: Dict[str, str] = field(default_factory=dict)
path: str = ''
executable_links: List[str] = field(default_factory=list)
executable_dir: Tuple[str] = ()
additional_items: List[str] = field(init=False, default_factory=list)
Die Data Class-Version ist deklarativer, hat weniger Code, unterstützt typing und enthält die anderen generierten Funktionen.
Danksagungen
Die folgenden Personen lieferten unschätzbare Beiträge während der Entwicklung dieser PEP und des Codes: Ivan Levkivskyi, Guido van Rossum, Hynek Schlawack, Raymond Hettinger und Lisa Roach. Ich danke ihnen für ihre Zeit und ihr Fachwissen.
Besonders erwähnt werden muss das Projekt attrs. Es war eine echte Inspiration für diese PEP, und ich schätze die Designentscheidungen, die sie getroffen haben.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0557.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT