Einführung in Datenklassen

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__ -Methode
  • compare 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) 

Source: https://habr.com/ru/post/de415829/


All Articles