Python-Datenklassen verstehen – Teil 1 | Komentor

Wenn Sie dies lesen, kennen Sie bereits Python 3.7 und die damit einhergehenden neuen Funktionen. Ich persönlich freue mich am meisten darüber Dataclasses. Ich warte schon eine Weile auf ihre Ankunft.

Dies ist ein zweiteiliger Beitrag:

  1. Überblick über die Dataclass-Funktionen in diesem Beitrag
  2. Datenklasse fields Übersicht im nächster Beitrag

Einführung

Dataclasses sind Python-Klassen, eignen sich aber zum Speichern von Datenobjekten. Was sind Datenobjekte, fragen Sie? Hier ist eine nicht erschöpfende Liste von Merkmalen, die Datenobjekte definieren:

  • Sie speichern Daten und repräsentieren einen bestimmten Datentyp. Bsp.: Eine Zahl. Für Personen, die mit ORMs vertraut sind, ist eine Modellinstanz ein Datenobjekt. Es repräsentiert eine bestimmte Art von Entität. Es enthält Attribute, die die Entität definieren oder darstellen.
  • Sie können mit anderen Objekten des gleichen Typs verglichen werden. Bsp.: Eine Zahl kann sein greater than, less thanoder equal to eine andere Nummer

Es gibt sicherlich noch mehr Funktionen, aber diese Liste reicht aus, um Ihnen zu helfen, die Krux zu verstehen.

Verstehen Dataclasseswerden wir eine einfache Klasse implementieren, die eine Zahl enthält und es uns ermöglicht, die oben genannten Operationen auszuführen.
Zuerst werden wir normale Klassen verwenden, und dann werden wir verwenden Dataclasses um das gleiche Ergebnis zu erzielen.

Aber bevor wir beginnen, ein Wort zur Verwendung von Dataclasses

Python 3.7 bietet einen Decorator Datenklasse die verwendet wird, um eine Klasse in eine Datenklasse umzuwandeln.

Alles, was Sie tun müssen, ist die Klasse in den Decorator zu packen:

from dataclasses import dataclass

@dataclass
class A:
 …

Lassen Sie uns nun in die Verwendung von wie und was eintauchen dataclass ändert sich für uns.

Initialisierung

Üblich

class Number:

__init__ (self, val):
 self.val = val
 
>>> one = Number(1)
>>> one.val
>>> 1

Mit dataclass

@dataclass
class Number:
 val:int 
 
>>> one = Number(1)
>>> one.val
>>> 1

Folgendes hat sich beim Dataclass-Decorator geändert:

  1. Keine Definition nötig __init__und dann Werte zuzuweisen self, d kümmert sich darum
  2. Wir haben die Mitgliedsattribute im Voraus in einer viel besser lesbaren Weise definiert, zusammen mit geben Sie Hinweise ein. Das wissen wir jetzt sofort val ist vom Typ int. Dies ist definitiv besser lesbar als die übliche Art, Klassenmitglieder zu definieren.

Zen of Python: Lesbarkeit zählt

Es ist auch möglich, Standardwerte zu definieren:

@dataclass
class Number:
    val:int = 0

Darstellung

Die Objektdarstellung ist eine aussagekräftige Zeichenfolgendarstellung des Objekts, die beim Debuggen sehr nützlich ist.

Die Standarddarstellung von Python-Objekten ist nicht sehr aussagekräftig:

class Number:
    def __init__ (self, val = 0):
    self.val = val
 
>>> a = Number(1)
>>> a
>>> < __main__.Number object at 0x7ff395b2ccc0>

Dies gibt uns keinen Einblick in die Nützlichkeit des Objekts und führt zu einer schrecklichen Debugging-Erfahrung.

Eine sinnvolle Darstellung könnte durch die Definition von a implementiert werden __repr__ Methode in der Klassendefinition.

def __repr__ (self):
    return self.val

Jetzt erhalten wir eine sinnvolle Darstellung des Objekts:

>>> a = Number(1)
>>> a
>>> 1

dataclass automatisch hinzufügen __repr__Funktion, damit wir sie nicht manuell implementieren müssen.

@dataclass
class Number:
    val: int = 0

>>> a = Number(1)
>>> a
>>> Number(val = 1)

Datenvergleich

Generell müssen Datenobjekte miteinander verglichen werden.

Vergleich zwischen zwei Objekten a und b besteht im Allgemeinen aus den folgenden Operationen:

  • a
  • a > b
  • ein == b
  • a >= b
  • a <= b

In Python ist es möglich zu definieren [methods]( lt ) in Klassen, die die oben genannten Operationen ausführen können. Der Einfachheit halber und um diesen Beitrag nicht Amok laufen zu lassen, werde ich nur die Implementierung von demonstrieren == und <.

Üblich

class Number:
    def __init__ ( self, val = 0):
       self.val = val
 
    def __eq__ (self, other):
        return self.val == other.val
 
    def __lt__ (self, other):
        return self.val < other.val

Mit dataclass

@dataclass(order = True)
class Number:
    val: int = 0

Ja, das ist es.

Wir brauchen die nicht zu definieren __eq__und __lt__ Methoden, weil dataclass decorator fügt sie automatisch der Klassendefinition für uns hinzu, wenn sie mit aufgerufen wird order = True

Nun, wie macht es das?

Wenn Sie verwenden dataclass, es fügt eine Funktion hinzu __eq__ und __lt__ zur Klassendefinition. Das wissen wir bereits. Woher wissen diese Funktionen also, wie sie die Gleichheit überprüfen und vergleichen können?

Eine generierte Datenklasse __eq__ Die Funktion vergleicht ein Tupel ihrer Attribute mit einem Tupel von Attributen der anderen Instanz derselben Klasse. In unserem Fall ist hier, was die automatically generiert __eq__ Funktion wäre äquivalent zu:

def __eq__ (self, other):
    return (self.val,) == (other.val,)

Schauen wir uns ein ausführlicheres Beispiel an:

Wir werden eine Datenklasse schreiben Personihre zu halten name und age.

@dataclass(order = True)
class Person:
    name: str
    age:int = 0

Die automatisch generierte __eq__ Methode ist äquivalent zu:

def __eq__ (self, other):
    return (self.name, self.age) == ( other.name, other.age)

Achten Sie auf die Reihenfolge der Attribute. Sie werden immer in der Reihenfolge generiert, in der Sie sie in der Datenklassendefinition definiert haben.

Ebenso das Äquivalent __le__ Funktion wäre ähnlich wie:

def __le__ (self, other):
    return (self.name, self.age) <= (other.name, other.age)

Eine Notwendigkeit zum Definieren einer Funktion wie __le__ tritt im Allgemeinen auf, wenn Sie eine Liste Ihrer Datenobjekte sortieren müssen. Python ist eingebaut sortiert Die Funktion beruht auf dem Vergleich zweier Objekte.

>>> import random

>>> a = [Number(random.randint(1,10)) for _ in range(10)] #generate list of random numbers

>>> a

>>> [Number(val=2), Number(val=7), Number(val=6), Number(val=5), Number(val=10), Number(val=9), Number(val=1), Number(val=10), Number(val=1), Number(val=7)]

>>> sorted_a = sorted(a) #Sort Numbers in ascending order

>>> [Number(val=1), Number(val=1), Number(val=2), Number(val=5), Number(val=6), Number(val=7), Number(val=7), Number(val=9), Number(val=10), Number(val=10)]

>>> reverse_sorted_a = sorted(a, reverse = True) #Sort Numbers in descending order

>>> reverse_sorted_a

>>> [Number(val=10), Number(val=10), Number(val=9), Number(val=7), Number(val=7), Number(val=6), Number(val=5), Number(val=2), Number(val=1), Number(val=1)]

dataclass als rufbarer Dekorateur

Es ist nicht immer wünschenswert, alle zu haben dunder Methoden definiert. Ihr Anwendungsfall besteht möglicherweise nur darin, die Werte zu speichern und die Gleichheit zu überprüfen. Sie benötigen also nur die __init__ und __eq__ Methoden definiert. Wenn wir dem Dekorateur sagen könnten, dass er die anderen Methoden nicht generieren soll, würde dies etwas Overhead reduzieren und wir würden korrekte Operationen für das Datenobjekt zur Verfügung haben.

Glücklicherweise kann dies durch die Verwendung von erreicht werden dataclass decorator als aufrufbar.

Vom Beamten Dokumentekann der Decorator als Callable mit den folgenden Argumenten verwendet werden:

[@dataclass]( "Twitter profile for @dataclass")(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
 …
  1. init : Standardmäßig ein __init__ Methode wird generiert. Wenn bestanden als Falsewird die Klasse keine haben __init__ Methode.
  2. repr : __repr__ -Methode wird standardmäßig generiert. Wenn bestanden als Falsewird die Klasse keine haben __repr__ Methode.
  3. eq: Standardmäßig die __eq__ Methode wird generiert. Wenn bestanden als Falsedas __eq__ Methode wird nicht hinzugefügt von dataclasswird aber standardmäßig auf die object. __eq__.
  4. order : Standardmäßig __gt__ , __ge__, __lt__, __le__ Methoden werden generiert. Wenn bestanden als Falsesie entfallen.

Wir werden diskutieren frozen in einer Weile. Das unsafe_hash argument verdient wegen seiner komplizierten Anwendungsfälle einen separaten Beitrag.

Nun zurück zu unserem Anwendungsfall, hier ist, was wir brauchen:

  1. __init__
  2. __eq__

Diese Funktionen werden standardmäßig generiert, also müssen wir die anderen Funktionen nicht generieren lassen. Wie machen wir das? Übergeben Sie einfach die relevanten Argumente als falsch an den Generator.

[@dataclass]( "Twitter profile for @dataclass")(repr = False) # order, unsafe_hash and frozen are False
class Number:
    val: int = 0

>>> a = Number(1)

>>> a

>>> < __main__.Number object at 0x7ff395afe898>

>>> b = Number(2)

>>> c = Number(1)

>>> a == b

>>> False

>>> a < b

>>> Traceback (most recent call last):
 File “<stdin>”, line 1, in <module>
TypeError: ‘<’ not supported between instances of ‘Number’ and ‘Number’

Eingefrorene Instanzen

Eingefrorene Instanzen sind Objekte, deren Attribute nicht geändert werden können, nachdem das Objekt initialisiert wurde.

Es ist nicht möglich, wirklich unveränderliche Python-Objekte zu erstellen

Unveränderliche Attribute für ein Objekt in Python zu erstellen, ist eine mühsame Aufgabe und etwas, auf das ich in diesem Beitrag nicht eingehen werde.

Folgendes erwarten wir von einem unveränderlichen Objekt:

>>> a = Number(10) #Assuming Number class is immutable

>>> a.val = 10 # Raises Error

Mit Dataclasses ist es möglich, ein eingefrorenes Objekt mit zu definieren dataclass decorator als Callable mit Argument frozen=True .

Wenn ein eingefrorenes Datenklassenobjekt instanziiert wird, wird jeder Versuch, die Attribute des Objekts zu ändern, ausgelöst FrozenInstanceError.

@dataclass(frozen = True)
class Number:
    val: int = 0

>>> a = Number(1)

>>> a.val

>>> 1

>>> a.val = 2

>>> Traceback (most recent call last):
 File “<stdin>”, line 1, in <module>
 File “<string>”, line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field ‘val’

Eine eingefrorene Instanz ist also eine großartige Möglichkeit zum Speichern

Diese ändern sich im Allgemeinen über die Lebensdauer der Anwendung nicht und jeder Versuch, sie zu ändern, sollte im Allgemeinen abgewehrt werden.

Post-Init-Verarbeitung

Bei Dataclasses ist die Anforderung, eine __init__ Methode zum Zuweisen von Variablen self wurde gesorgt. Aber jetzt verlieren wir die Flexibilität, Funktionsaufrufe/Verarbeitungen vorzunehmen, die unmittelbar nach der Zuweisung der Variablen erforderlich sein könnten.

Lassen Sie uns einen Anwendungsfall diskutieren, in dem wir eine Klasse definieren Float Fließkommazahlen enthalten, und wir berechnen die ganzzahligen und dezimalen Teile sofort nach der Initialisierung.

Üblich

import math

class Float:
    def __init__ (self, val = 0):
        self.val = val
        self.process()
 
    def process(self):
        self.decimal, self.integer = math.modf(self.val)
 
>>> a = Float( 2.2)

>>> a.decimal

>>> 0.2000

>>> a.integer

>>> 2.0

Glücklicherweise ist die Verarbeitung nach der Initialisierung bereits erledigt __post_init__ Methode.

Das Erzeugte __init__ Methode ruft die auf __post_init__ Methode vor der Rückkehr. In diesen Funktionen kann also jede beliebige Verarbeitung vorgenommen werden.

import math

@dataclass
class FloatNumber:
    val: float = 0.0
 
    def __post_init__ (self):
        self.decimal, self.integer = math.modf(self.val)
 
>>> a = Number(2.2)

>>> a.val

>>> 2.2

>>> a.integer

>>> 2.0

>>> a.decimal

>>> 0.2

Sauber!

Nachlass

Dataclasses unterstützt die Vererbung wie normale Python-Klassen.

Die in der übergeordneten Klasse definierten Attribute sind also in der untergeordneten Klasse verfügbar.

@dataclass
class Person:
    age: int = 0
    name: str

@dataclass
class Student(Person):
    grade: int

>>> s = Student(20, "John Doe", 12)

>>> s.age

>>> 20

>>> s.name

>>> "John Doe"

>>> s.grade

>>> 12

Achten Sie darauf, dass die Argumente zu Student sind in der Reihenfolge der Felder, die in der Klassendefinition definiert sind.

Was ist mit dem Verhalten von __post_init__ beim erben?

Seit __post_init__ nur eine weitere Funktion ist, muss sie in der herkömmlichen Form aufgerufen werden:

@dataclass
class A:
    a: int
    
    def __post_init__ (self):
        print("A")

@dataclass
class B(A):
    b: int
    
    def __post_init__ (self):
        print("B")

>>> a = B(1,2)

>>> B

Nur im obigen Beispiel B's __post_init__ wird genannt. Wie rufen wir auf A's __post_init__ ?

Da es sich um eine Funktion der übergeordneten Klasse handelt, kann sie mit aufgerufen werden super.

@dataclass
class B(A):
    b: int
    
    def __post_init__ (self):
        super(). __post_init__ () #Call post init of A
        print("B")

>>> a = B(1,2)

>>> A
    B

Fazit

Oben sind also einige Möglichkeiten aufgeführt, wie Dataclasses Python-Entwicklern das Leben erleichtern.

Ich habe versucht, gründlich zu sein und die meisten Anwendungsfälle abzudecken, aber kein Mann ist perfekt. Melden Sie sich, wenn Sie Fehler finden oder möchten, dass ich auf relevante Anwendungsfälle achte.

Ich werde abdecken Datenklassen.Feld und unsafe_hash in verschiedenen Beiträgen.

Folge mir auf GitHub, Twitter.

Update: Beitrag für dataclasses.field kann gefunden werden hier.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *