Eine der neuen Funktionen, die in Python 3.7 eingeführt wurden, sind die Datenklassen. Sie sollen die Generierung von Code für Klassen automatisieren, die zum Speichern von Daten verwendet werden. Trotz der Tatsache, dass sie andere Arbeitsmechanismen verwenden, können sie mit "veränderlichen benannten Tupeln mit Standardwerten" verglichen werden.
Einführung
Für alle oben genannten Beispiele ist Python 3.7 oder höher erforderlich.
Die meisten Python-Entwickler müssen diese Klassen regelmäßig schreiben:
class RegularBook: def __init__(self, title, author): self.title = title self.author = author
Bereits in diesem Beispiel ist Redundanz sichtbar. Die Titel- und Autorenkennungen werden mehrmals verwendet. Die reale Klasse enthält auch überschriebene Methoden __eq__
und __repr__
.
Das dataclasses
enthält den @dataclass
Dekorator. Wenn Sie es verwenden, würde ein ähnlicher Code folgendermaßen aussehen:
from dataclasses import dataclass @dataclass class Book: title: str author: str
Es ist wichtig zu beachten, dass Typanmerkungen erforderlich sind . Alle Felder ohne Typmarkierungen werden ignoriert. Wenn Sie keinen bestimmten Typ verwenden möchten, können Sie natürlich Any
aus dem typing
angeben.
Was bekommen Sie als Ergebnis? Sie erhalten automatisch eine Klasse mit den implementierten Methoden __init__
, __repr__
, __str__
und __eq__
. Außerdem handelt es sich um eine reguläre Klasse, von der Sie erben oder beliebige Methoden hinzufügen können.
>>> book = Book(title="Fahrenheit 451", author="Bradbury") >>> book Book(title='Fahrenheit 451', author='Bradbury') >>> book.author 'Bradbury' >>> other = Book("Fahrenheit 451", "Bradbury") >>> book == other True
Alternativen
Tupel oder Wörterbuch
Wenn die Struktur recht einfach ist, können Sie die Daten natürlich in einem Wörterbuch oder Tupel speichern:
book = ("Fahrenheit 451", "Bradbury") other = {'title': 'Fahrenheit 451', 'author': 'Bradbury'}
Dieser Ansatz hat jedoch Nachteile:
- Es ist zu beachten, dass die Variable Daten enthält, die sich auf diese Struktur beziehen.
- Bei einem Wörterbuch müssen Sie die Namen der Schlüssel nachverfolgen. Eine solche Initialisierung des Wörterbuchs
{'name': 'Fahrenheit 451', 'author': 'Bradbury'}
ist auch formal korrekt. - Im Falle eines Tupels müssen Sie die Reihenfolge der Werte verfolgen, da diese keine Namen haben.
Es gibt eine bessere Option:
Namedtuple
from collections import namedtuple NamedTupleBook = namedtuple("NamedTupleBook", ["title", "author"])
Wenn wir die auf diese Weise erstellte Klasse verwenden, erhalten wir praktisch das Gleiche wie die Datenklasse.
>>> book = NamedTupleBook("Fahrenheit 451", "Bradbury") >>> book.author 'Bradbury' >>> book NamedTupleBook(title='Fahrenheit 451', author='Bradbury') >>> book == NamedTupleBook("Fahrenheit 451", "Bradbury")) True
Trotz der allgemeinen Ähnlichkeit haben benannte Tupel ihre Grenzen. Sie kommen von der Tatsache, dass benannte Tupel immer noch Tupel sind.
Erstens können Sie immer noch Instanzen verschiedener Klassen vergleichen.
>>> Car = namedtuple("Car", ["model", "owner"]) >>> book = NamedTupleBook("Fahrenheit 451", "Bradbury")) >>> book == Car("Fahrenheit 451", "Bradbury") True
Zweitens sind benannte Tupel unveränderlich. In einigen Situationen ist dies nützlich, aber ich möchte mehr Flexibilität.
Schließlich können Sie sowohl ein benanntes als auch ein reguläres Tupel bearbeiten. Zum Beispiel iterieren.
Andere Projekte
Wenn nicht auf die Standardbibliothek beschränkt, finden Sie andere Lösungen für dieses Problem. Insbesondere das Projekt attrs . Es kann sogar mehr als nur Datenklassen und funktioniert mit älteren Python-Versionen wie 2.7 und 3.4. Die Tatsache, dass es nicht Teil der Standardbibliothek ist, kann jedoch unpraktisch sein
Schöpfung
@dataclass
Dekorator @dataclass
können Sie eine Datenklasse erstellen. In diesem Fall werden alle Felder der mit Typanmerkung definierten Klasse in den entsprechenden Methoden der resultierenden Klasse verwendet.
Alternativ gibt es die Funktion make_dataclass
, die ähnlich wie das Erstellen benannter Tupel funktioniert.
from dataclasses import make_dataclass Book = make_dataclass("Book", ["title", "author"]) book = Book("Fahrenheit 451", "Bradbury")
Standardwerte
Eine nützliche Funktion ist das einfache Hinzufügen von Standardwerten zu Feldern. __init__
Methode __init__
immer noch nicht neu definiert werden. __init__
einfach die Werte direkt in der Klasse an.
@dataclass class Book: title: str = "Unknown" author: str = "Unknown author"
Sie werden bei der generierten __init__
-Methode berücksichtigt
>>> Book() Book(title='Unknown', author='Unknown author') >>> Book("Farenheit 451") Book(title='Farenheit 451', author='Unknown author')
Aber wie bei regulären Klassen und Methoden müssen Sie vorsichtig sein, wenn Sie veränderbare Standardeinstellungen verwenden. Wenn Sie beispielsweise die Liste als Standardwert verwenden müssen, gibt es einen anderen Weg, aber mehr dazu weiter unten.
Darüber hinaus ist es wichtig, die Reihenfolge zu überwachen, in der Felder mit Standardwerten ermittelt werden, da diese genau mit der Reihenfolge in der Methode __init__
Unveränderliche Datenklassen
Instanzen benannter Tupel sind unveränderlich. In vielen Situationen ist dies eine gute Idee. Für Datenklassen können Sie dies auch tun. FrozenInstanceError
Sie beim Erstellen der Klasse einfach den Parameter frozen=True
Wenn Sie versuchen, ihre Felder zu ändern, wird eine FrozenInstanceError
Ausnahme FrozenInstanceError
@dataclass(frozen=True) class Book: title: str author: str
>>> book = Book("Fahrenheit 451", "Bradbury") >>> book.title = "1984" dataclasses.FrozenInstanceError: cannot assign to field 'title'
Datenklasseneinstellung
Zusätzlich zum frozen
Parameter verfügt der @dataclass
Dekorator über weitere Parameter:
init
: Wenn es True
(Standard), wird die Methode __init__
generiert. Wenn für die Klasse bereits eine __init__
-Methode definiert ist, wird der Parameter ignoriert.repr
: __repr__
(standardmäßig) die Erstellung der __repr__
-Methode. Die generierte Zeichenfolge enthält den Klassennamen sowie den Namen und die Darstellung aller in der Klasse definierten Felder. In diesem Fall können einzelne Felder ausgeschlossen werden (siehe unten)eq
: __eq__
(standardmäßig) die Erstellung der __eq__
-Methode. Objekte werden auf die gleiche Weise verglichen, als wären sie Tupel mit den entsprechenden Feldwerten. Zusätzlich wird die Typübereinstimmung überprüft.order
aktiviert (Standard ist __lt__
) die Erstellung der __ge__
__lt__
, __le__
, __gt__
und __ge__
. Objekte werden auf die gleiche Weise wie die entsprechenden Tupel von Feldwerten verglichen. Gleichzeitig wird auch die Art der Objekte überprüft. Wenn order
angegeben ist, eq
jedoch nicht, wird eine ValueError
Ausnahme ausgelöst. Außerdem sollte die Klasse keine bereits definierten Vergleichsmethoden enthalten.unsafe_hash
wirkt sich auf die Generierung der __hash__
-Methode aus. Das Verhalten hängt auch von den Werten der Parameter eq
und frozen
Passen Sie einzelne Felder an
In den meisten Standardsituationen ist dies nicht erforderlich, es ist jedoch möglich, das Verhalten der Datenklasse mithilfe der Feldfunktion an einzelne Felder anzupassen.
Änderbare Standardeinstellungen
Eine typische Situation, die oben erwähnt wurde, ist die Verwendung von Listen oder anderen veränderlichen Standardwerten. Möglicherweise möchten Sie eine "Bücherregal" -Klasse mit einer Liste von Büchern. Wenn Sie den folgenden Code ausführen:
@dataclass class Bookshelf: books: List[Book] = []
Der Dolmetscher meldet einen Fehler:
ValueError: mutable default <class 'list'> for field books is not allowed: use default_factory
Bei anderen veränderlichen Werten funktioniert diese Warnung jedoch nicht und führt zu einem falschen Programmverhalten.
Um Probleme zu vermeiden, wird empfohlen, den Parameter default_factory
der default_factory
zu verwenden. Sein Wert kann ein beliebiges aufgerufenes Objekt oder eine Funktion ohne Parameter sein.
Die richtige Version der Klasse sieht folgendermaßen aus:
@dataclass class Bookshelf: books: List[Book] = field(default_factory=list)
Andere Optionen
Zusätzlich zur angegebenen default_factory
verfügt die default_factory
über die folgenden Parameter:
default
: Der default
. Dieser Parameter ist erforderlich, da der Aufruf des field
den Standardfeldwert ersetzt.init
: __init__
(Standard) die Verwendung eines Felds in der Methode __init__
repr
: __repr__
(Standard) die Verwendung eines Felds in der __repr__
-Methodecompare
beinhaltet (Standard) die Verwendung des Feldes in Vergleichsmethoden ( __eq__
, __le__
und andere)hash
: Kann ein boolescher Wert oder None
. Wenn dies der True
, wird das Feld zur Berechnung des Hashs verwendet. Wenn (standardmäßig) None
angegeben None
, wird der Wert des compare
verwendet.
Einer der Gründe für die Angabe von hash=False
für einen bestimmten compare=True
kann die Schwierigkeit sein, den Feld-Hash zu berechnen, während er für den Vergleich erforderlich ist.metadata
: Benutzerdefiniertes Wörterbuch oder None
. Der Wert wird in MappingProxyType
sodass er unveränderlich wird. Dieser Parameter wird von den Datenklassen selbst nicht verwendet und ist für Erweiterungen von Drittanbietern vorgesehen.
Verarbeitung nach der Initialisierung
Die automatisch generierte Methode __init__
ruft die Methode __post_init__
, sofern sie in der Klasse definiert ist. In der Regel wird es in der Form self.__post_init__()
jedoch Variablen vom Typ InitVar
in der Klasse definiert InitVar
, werden sie als Methodenparameter übergeben.
Wenn die Methode __init__
nicht generiert wurde, wird __post_init__
nicht aufgerufen.
Fügen Sie beispielsweise eine generierte Buchbeschreibung hinzu
@dataclass class Book: title: str author: str desc: str = None def __post_init__(self): self.desc = self.desc or "`%s` by %s" % (self.title, self.author)
>>> Book("Fareneheit 481", "Bradbury") Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury')
Parameter nur zur Initialisierung
Eine der mit der Methode __post_init__
verbundenen Möglichkeiten sind die Parameter, die nur für die Initialisierung verwendet werden. Wenn Sie beim Deklarieren eines Felds InitVar
als Typ angeben, wird sein Wert als Parameter der Methode __post_init__
. In keiner anderen Weise werden solche Felder in der Datenklasse verwendet.
@dataclass class Book: title: str author: str gen_desc: InitVar[bool] = True desc: str = None def __post_init__(self, gen_desc: str): if gen_desc and self.desc is None: self.desc = "`%s` by %s" % (self.title, self.author)
>>> Book("Fareneheit 481", "Bradbury") Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury') >>> Book("Fareneheit 481", "Bradbury", gen_desc=False) Book(title='Fareneheit 481', author='Bradbury', desc=None)
Vererbung
Wenn Sie den Dekorator @dataclass
, werden alle übergeordneten Klassen beginnend mit object durchlaufen. Für jede gefundene Datenklasse werden die Felder in einem geordneten Wörterbuch gespeichert und anschließend die Eigenschaften der verarbeiteten Klasse hinzugefügt. Alle generierten Methoden verwenden Felder aus dem resultierenden geordneten Wörterbuch.
Wenn die übergeordnete Klasse Standardwerte definiert, müssen Sie daher die Felder mit Standardwerten definieren.
Da das geordnete Wörterbuch die Werte in Einfügereihenfolge für die folgenden Klassen speichert
@dataclass class BaseBook: title: Any = None author: str = None @dataclass class Book(BaseBook): desc: str = None title: str = "Unknown"
Eine __init__
-Methode mit dieser Signatur wird generiert:
def __init__(self, title: str="Unknown", author: str=None, desc: str=None)