Klassen — Daten und Verhalten zusammen
`class`, `__init__` und `self`, Methoden, eine druckbare Form mit `__repr__`, Gleichheit mit `__eq__`, Klassenebene gegen Instanzebene, und wann eine Klasse nicht gewünscht ist.
Wie möchtest du lernen?
Das Problem, das wir lösen
Bis jetzt haben wir mehrere Fakten über eine Sache in einem Dictionary gehalten.
order = {"name": "pen", "price": 15.0, "quantity": 3}
print(order["nmae"])KeyError: 'nmae'Der Fehler wurde ausgelöst, was immerhin etwas ist — aber erst, wenn diese Zeile läuft. Versteckt in einer Bedingung, könnte er erst einen Monat später auftauchen.
Und das Problem ist nicht nur die Schreibweise. Jeder Schlüssel kann in ein Dictionary gesteckt werden, also sagt nirgendwo, was eine Bestellzeile eigentlich enthalten soll. Die Funktion, die sie summiert, steht woanders, und niemand prüft, ob das Dictionary alle Felder hat, bevor sie aufgerufen wird. Die Daten sind an einem Ort, was man damit tun kann an einem anderen, und was zusammengehört, existiert nur in deinem Kopf.
class OrderLine:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
def total(self):
return round(self.price * self.quantity, 2)
line = OrderLine("pen", 15.0, 3)
print(line.name)
print(line.total())pen
45.0Eine Klasse ist eine Schablone: sie sagt, was ein Ding dieser Art enthält, und was man damit tun kann.
Am Ende dieses Kapitels kannst du
- Eine
classschreiben und sagen, was__init__undselftun - Methoden schreiben und sagen, wie sie sich von normalen Funktionen unterscheiden
- Einem Objekt eine druckbare Form mit
__repr__geben - Sagen, was ein Klassenattribut von einem Instanzattribut trennt — und eine bekannte Falle vermeiden
- Zwei Objekte mit
__eq__vergleichbar machen - Sagen, wann eine Klasse nicht gewünscht ist
Was du vorher brauchst: JSON und CSV.
Was self wirklich ist
self ist kein Zauberwort — es ist der erste Parameter, und was darin landet, ist das Objekt, auf dem die Methode aufgerufen wurde.
line.total() zu schreiben bedeutet: „Führe die total-Methode von OrderLine aus, und gib ihr line als self“. Daher gibt self.price darin genau den Preis dieser speziellen Zeile.
Vergisst du self, ist die Meldung zuerst verwirrend:
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())TypeError: OrderLine.shout() takes 0 positional arguments but 1 was given„1 was given“ — aber die Klammern waren leer. Was gegeben wurde, ist line selbst, stillschweigend, und shout ließ keinen Platz, um es entgegenzunehmen.
__init__ ist auch eine ganz normale Methode, nur mit einem speziellen Namen: Python ruft sie direkt nach der Erstellung des Objekts auf. Darin setzt self.name = name die Attribute.
Ein Tippfehler gibt einen klareren Fehler
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(line.nmae)AttributeError: 'OrderLine' object has no attribute 'nmae'Um ehrlich zu sein: eine Klasse fängt diesen Fehler nicht früher ab. Ein AttributeError, genau wie ein KeyError, erscheint erst, wenn die Zeile läuft. Versteckt in einer Bedingung, könnte auch dieser erst einen Monat später auftauchen.
Wo liegt also der Gewinn? An drei Stellen.
- Die Meldung sagt mehr. Die Fehlermeldung nennt die Klasse —
'OrderLine' object— so siehst du, auf was für ein Ding der falsche Name angewendet wurde. - Was da sein soll, steht an einem Ort. Das Lesen von
__init__zeigt, welche Attribute einOrderLinehat. Ein Dictionary hat keine solche Liste. - Dein Editor kann helfen. Ein Editor liest die Klasse und schlägt Attributnamen vor, nachdem du
line.tippst, und viele Editoren unterstreichen einen unbekannten Namen — bevor der Code überhaupt läuft. Bei Dictionary-Schlüsseln kann ein Editor das meistens nicht.
Denk auch an die Grenze: ein Tippfehler wird beim Schreiben nicht erkannt. Genau wie order["nmae"] = 5 stillschweigend einen neuen Schlüssel macht, macht line.nmae = 5 stillschweigend ein neues Attribut.
Und was drinnen ist, kann man ansehen:
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(type(line))
print(isinstance(line, OrderLine))
print(line.__dict__)<class '__main__.OrderLine'>
True
{'name': 'pen'}__dict__ zeigt, dass da wirklich ein Dictionary im Objekt steckt. Die Klasse hat das Dictionary nicht entfernt — sie gab ihm einen Namen und eine Struktur.
__repr__ — was print zeigt
Ohne __repr__:
class OrderLine:
def __init__(self, name):
self.name = name
print(OrderLine("pen"))<__main__.OrderLine object at 0x79b769f03830>Die Zahl am Ende ist eine Speicheradresse, und sie unterscheidet sich bei jedem Lauf — auf deinem Bildschirm siehst du eine andere Zahl. Beim Debuggen ist sie nutzlos.
class OrderLine:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"OrderLine(name={self.name!r}, price={self.price!r})"
line = OrderLine("pen", 15.0)
print(line)
print([line, line])OrderLine(name='pen', price=15.0)
[OrderLine(name='pen', price=15.0), OrderLine(name='pen', price=15.0)]Die zweite Zeile ist der eigentliche Nutzen: sie funktioniert auch in einer Liste. Wenn Python eine Liste druckt, ruft es das __repr__ jedes Elements auf, nicht sein __str__. Deswegen ist __repr__ von den beiden das, das man zuerst schreibt.
Die Konvention ist, einen String zurückzugeben, der aussieht wie der Code, der das Objekt erstellt hat. !r zu benutzen, setzt Anführungszeichen um die Strings darin — aus demselben Grund wie in Kapitel 24.
Gleichheit — der Standard ist nicht das, was du willst
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)False
TrueDie beiden Objekte halten dasselbe, und trotzdem gibt == False. Weil die Standardgleichheit ist Identität — == verhält sich genau wie is und fragt: „Ist das exakt dasselbe Objekt?“.
Das zu ändern heißt zu sagen, was gleich bedeutet:
class OrderLine:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a is b)True
FalseJetzt sagt == „ähnlich“ und is „dasselbe Objekt“ — die Unterscheidung aus Kapitel 21, diesmal in einem eigenen Typ.
Ein Hinweis: __eq__ zu schreiben lässt das Objekt standardmäßig unhashbar, also kann es nicht in ein Set und kein Dictionary-Schlüssel sein. Das nächste Kapitels dataclass erledigt beides in einer Zeile.
Klassenebene und Instanzebene
class Basket:
TAX_RATE = 0.15
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.items is b.items)['pen', 'bag']
['pen', 'bag']
TrueZwei getrennte Körbe, aber nur eine Liste.
Erkennst du es? Das ist die veränderliche-Standard-Falle aus Kapitel 21, an einem neuen Ort. Die Zeile items = [] direkt unter der class-Zeile läuft einmal, wenn die Klasse erstellt wird — nicht einmal pro Objekt. Also gehört die Liste der Klasse, und jede Instanz teilt sie.
Die Korrektur ist dieselbe Art: was jedem Objekt gehört, baue in __init__ auf.
class Basket:
TAX_RATE = 0.15
def __init__(self):
self.items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.TAX_RATE, b.TAX_RATE)['pen']
['bag']
0.15 0.15TAX_RATE blieb auf Klassenebene, und das zu Recht — es ist für jeden Korb gleich, und wir wollen es nicht ändern. Aber beachte, dass Python dich nicht aufhält, es zu ändern: Basket.TAX_RATE = 0.2 wirft keinen Fehler. Der Großbuchstaben-Name ist nur eine Konvention, die dem Leser sagt: „Behandle das als Konstante, ändere es nicht“. Die Regel in einer Zeile: Konstanten auf Klassenebene; alles, was sich ändert, in __init__.
Wann man keine Klasse schreibt
Sobald man Klassen gelernt hat, ist es verlockend, aus allem eine Klasse zu machen. Ein paar Zeichen, dass die Klasse nicht gebraucht wird:
Die Klasse hat nur ein __init__ und eine Methode. Das ist eine Funktion, ohne Grund in zwei Teile geteilt.
Keine Methoden, nur Daten. Das nächste Kapitels dataclass ist genau dafür gemacht, in weit weniger Zeilen.
Das Objekt ändert sich nie und hält eine Sache. Ein Tupel oder ein einfacher Wert reicht.
Eine Klasse ist das richtige Werkzeug, wo Daten und Verhalten wirklich zusammengehören: einige Daten, die sich ändern, und ein paar Operationen, die immer auf diesen Daten laufen.
Ein vollständiges Beispiel
"""An order built from two small classes."""
TAX_RATE = 0.15
class OrderLine:
"""One item on an order: what it is, what it costs, how many."""
def __init__(self, name, price, quantity):
if quantity < 1:
raise ValueError(f"quantity must be at least 1: {quantity}")
self.name = name
self.price = price
self.quantity = quantity
def total(self):
"""Price for this line, tax included."""
return round(self.price * self.quantity * (1 + TAX_RATE), 2)
def __repr__(self):
return f"OrderLine({self.name!r}, {self.price!r}, {self.quantity!r})"
class Order:
"""A named collection of lines, and the questions you ask about it."""
def __init__(self, customer):
self.customer = customer
self.lines = []
def add(self, name, price, quantity):
self.lines.append(OrderLine(name, price, quantity))
return self
def total(self):
return round(sum(line.total() for line in self.lines), 2)
def largest(self):
"""The most expensive line, or None for an empty order."""
if not self.lines:
return None
return max(self.lines, key=lambda line: line.total())
def __repr__(self):
return f"Order({self.customer!r}, {len(self.lines)} lines)"
def main():
order = Order("rafi")
order.add("pen", 15.0, 3)
order.add("bag", 850.0, 1)
order.add("ink", 120.0, 2)
for line in order.lines:
print(f"{line.name:<6} {line.total():>9.2f}")
print(f"{'total':<6} {order.total():>9.2f}")
print()
print(order)
print("largest:", order.largest())
try:
order.add("clip", 5.0, 0)
except ValueError as err:
print("rejected:", err)
if __name__ == "__main__":
main()pen 51.75
bag 977.50
ink 276.00
total 1305.25
Order('rafi', 3 lines)
largest: OrderLine('bag', 850.0, 1)
rejected: quantity must be at least 1: 0Vier Dinge, die einen Blick wert sind.
__init__ validiert. Mit quantity < 1 wird das Objekt gar nicht erst erstellt. Das ist das Nützlichste, was eine Klasse bietet: ein OrderLine zu haben heißt, dass seine Menge mindestens eins ist — an keiner Stelle, wo es benutzt wird, muss noch einmal geprüft werden. Ein Dictionary kann diese Garantie nicht geben.
Order weiß nicht, wie eine Zeile bepreist wird. Es ruft line.total() auf und das ist alles. Wie Steuer angewendet wird, ist OrderLine-Sache, und das zu ändern ändert einen Ort.
Beide Klassen haben ein __repr__, und die Ausgabe von largest() zeigt, warum es wichtig ist. Ohne wäre die letzte Zeile eine Speicheradresse gewesen, die dir nichts sagt.
add gibt sich selbst zurück (return self). Das macht order.add(...).add(...) möglich. Es wird hier bewusst nicht benutzt — die Option zu haben und jede Zeile damit zu strecken, sind verschiedene Dinge.
Wenn es schiefgeht
TypeError: ... takes 0 positional arguments but 1 was given Der Methode fehlt self. Füge self als ersten Parameter hinzu.
AttributeError: 'X' object has no attribute 'y' Ein Tippfehler, oder self.y wurde in __init__ nie gesetzt. print(obj.__dict__) zeigt, was wirklich da ist.
TypeError: X() takes no arguments Es gibt kein __init__, oder der Name ist falsch geschrieben (__init__, nicht __int__, zwei Unterstriche auf jeder Seite).
Alle Objekte teilen sich eine Liste Die Liste steht auf Klassenebene. Verschiebe sie in __init__.
Zwei Objekte halten dasselbe und == gibt False Die Standardgleichheit ist Identität. Schreib __eq__ — oder nutze das nächste Kapitels dataclass.
Nach dem Schreiben von __eq__ kann es nicht in ein Set __eq__ zu schreiben entfernt den Standard-Hash. dataclass(frozen=True) gibt beides.
print gibt <__main__.X object at 0x...> Es gibt kein __repr__.
Schritt 4 von 6 — Vorhersagen
Prüfe dein Verständnis
shout() wird ohne Argumente aufgerufen. Was passiert?
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())- AEin `TypeError` — es nimmt eins an, und null wurden gegeben
- BEin `TypeError` — es nimmt null an, und eins wurde gegeben
- CEin `AttributeError`
- D`hello` wird gedruckt
Zwei getrennte Körbe, ein Element in jedem. Was wird gedruckt?
class Basket:
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)- A['pen', 'bag'] []
- B['pen'] ['pen', 'bag']
- C['pen'] ['bag']
- D['pen', 'bag'] ['pen', 'bag']
Die beiden Objekte halten denselben Namen. Was wird gedruckt?
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)- AFalse True
- BEin `TypeError`
- CFalse False
- DTrue True
Zum Antworten brauchst du ein Konto
Melde dich an, um deine Antworten zu prüfen
Die Fragen stehen oben, und sie im Kopf durchzuarbeiten ist der wichtige Teil. Melde dich an, um die Antworten, die Erklärungen und die dreistufigen Hinweise zu sehen.
Jetzt bist du dran
Schreib zwei Klassen.
Book — gebaut aus title, author und pages. Es wirft einen ValueError, wenn pages kleiner als eins ist. Gib ihm ein __repr__, und ein __eq__, das zwei Bücher gleich nennt, wenn Titel und Autor übereinstimmen.
Shelf — sammelt Bücher, und bietet add(book), total_pages(), longest(), und by_author(name), das eine Liste zurückgibt.
Dann fünf Experimente:
- Verschiebe
Shelf'sbooks = []aus__init__unter dieclass-Zeile. Erstelle zwei getrennte Regale, füge einem ein Buch hinzu, und drucke dann das andere. Was siehst du? - Lösch das
__repr__und drucke eine Liste von Büchern. Wie viel schwerer wird das Debuggen? - Lösch
__eq__und vergleiche zwei Bücher mit demselben Titel und Autor. - Entferne
selfaus einer Methode und ruf sie auf. Wie viele Argumente sagt die Meldung, wurden gegeben? - Erstelle ein
Bookund schreibbook.pagse = 100(absichtlich falsch geschrieben). Gibt es einen Fehler? Dannprint(book.__dict__)— was siehst du?
Das letzte Experiment ist wichtig, weil es die Grenze einer Klasse zeigt. Ein falscher Name wirft einen Fehler beim Lesen, nicht beim Schreiben — ein neues Attribut zu setzen ist immer erlaubt. Das nächste Kapitel zeigt, dass Typ-Hinweise und ein Typ-Prüfer diese Lücke auch schließen können.
Schritt 6 von 6
Herausforderung — das Kapitel-Quiz
Zehn Fragen von leicht bis schwer. Die letzten sind absichtlich knifflig.
Melde dich an, um das Quiz zu machen