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

Python Enhancement Proposals

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

Inhaltsverzeichnis

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. Wenn order wahr ist und eq falsch ist, wird ein ValueError ausgelöst.

    Wenn die Klasse bereits eine der Methoden __lt__, __le__, __gt__ oder __ge__ definiert, wird ein ValueError ausgelöst.

  • unsafe_hash: Wenn False (Standard), wird die Methode __hash__ gemäß den Einstellungen von eq und frozen generiert.

    Wenn eq und frozen beide wahr sind, generiert Data Classes eine __hash__-Methode für Sie. Wenn eq wahr ist und frozen falsch ist, wird __hash__ auf None gesetzt, was sie als nicht hashbar markiert (was sie ist). Wenn eq falsch ist, bleibt __hash__ unverändert, was bedeutet, dass die __hash__-Methode der Superklasse verwendet wird (wenn die Superklasse object ist, bedeutet dies, dass sie auf id-basierte Hashing zurückfällt).

    Obwohl nicht empfohlen, können Sie Data Classes zwingen, eine __hash__-Methode mit unsafe_hash=True zu 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 als None definiert ist.
    • __eq__ in der Klasse definiert ist und ein __hash__ ungleich None definiert ist.
    • __eq__ nicht in der Klasse definiert ist und ein __hash__ definiert ist.

    Wenn unsafe_hash wahr ist und ein explizit definiertes __hash__ vorhanden ist, wird ein ValueError ausgelöst.

    Wenn unsafe_hash falsch 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 ein ValueError ausgelö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 der field-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, sowohl default als auch default_factory anzugeben.
  • 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 oder None sein. Wenn wahr, wird dieses Feld in die generierte __hash__-Methode aufgenommen. Wenn None (Standard), wird der Wert von compare verwendet: 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 als None wird nicht empfohlen.

    Ein möglicher Grund für hash=False, aber compare=True wä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 in types.MappingProxyType eingepackt, 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, compare und metadata haben die identische Bedeutung und Werte wie in der Deklaration field().

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 von Field-Objekten zurück, die die Felder für diese Data Class definieren. Akzeptiert entweder eine Data Class oder eine Instanz einer Data Class. Löst ValueError aus, wenn keine Data Class oder eine Instanz davon übergeben wird. Gibt keine Pseudo-Felder zurück, die ClassVar oder InitVar sind.
  • asdict(instance, *, dict_factory=dict): Konvertiert die Data Class instance in ein Dict (unter Verwendung der Factory-Funktion dict_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 TypeError aus, wenn instance keine Data Class-Instanz ist.

  • astuple(*, tuple_factory=tuple): Konvertiert die Data Class instance in ein Tupel (unter Verwendung der Factory-Funktion tuple_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 TypeError aus, wenn instance keine Data Class-Instanz ist.

  • make_dataclass(cls_name, fields, *, bases=(), namespace=None): Erstellt eine neue Data Class mit dem Namen cls_name, Feldern wie in fields definiert, Basisklassen wie in bases angegeben und initialisiert mit einem Namespace wie in namespace angegeben. fields ist ein iterierbares Element, dessen Elemente entweder name, (name, type) oder (name, type, Field) sind. Wenn nur name angegeben wird, wird typing.Any für type verwendet. Diese Funktion ist nicht unbedingt erforderlich, da jeder Python-Mechanismus zur Erstellung einer neuen Klasse mit __annotations__ dann die Funktion dataclass anwenden kann, um diese Klasse in eine Data Class zu konvertieren. Diese Funktion wird als Bequemlichkeit angeboten. Zum Beispiel
    C = 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 wie instance und ersetzt Felder durch Werte aus changes. Wenn instance keine Data Class ist, wird TypeError ausgelöst. Wenn Werte in changes keine Felder angeben, wird TypeError ausgelö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 replace angegeben werden, damit sie an __init__ und __post_init__ übergeben werden können.

    Es ist ein Fehler, wenn changes Felder enthält, die als init=False definiert sind. In diesem Fall wird ein ValueError ausgelöst.

    Seien Sie sich bewusst, wie init=False Felder bei einem Aufruf von replace() funktionieren. Sie werden nicht vom Quellobjekt kopiert, sondern in __post_init__() initialisiert, falls sie überhaupt initialisiert werden. Es wird erwartet, dass init=False Felder selten und sparsam verwendet werden. Wenn sie verwendet werden, ist es ratsam, alternative Klassenkonstruktoren zu haben, oder vielleicht eine benutzerdefinierte Methode replace() (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 zu Time hinzuzufü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


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

Zuletzt geändert: 2025-02-01 08:55:40 GMT