Klassen und Daten in Klassen
Kurz gesagt: Eine verschachtelte Klasse ist eine Klasse, die innerhalb einer anderen Klasse deklariert ist, weil sie nur dort Sinn ergibt; in Python werden auch Daten oft verschachtelt, als Dictionaries und Listen ineinander.
Problem
Eine Klasse, die nur an einer Stelle Sinn ergibt
In der Lektion über die verkettete Liste war Node eine eigene Klasse. Jeder im Programm kann Knoten anlegen, aber sie ergeben nur in der Liste Sinn. Genauso ist es mit einer Zeile der Bestellung:
class OrderLine: def __init__(self, product, quantity, price): self.product = product self.quantity = quantity self.price = price
class Order: def __init__(self): self.lines = []OrderLine ohne Order ergibt keinen Sinn. Der Name ist lang, damit er nicht mit der Zeile einer Rechnung, InvoiceLine, oder mit der Zeile im Lager zusammenstößt. Und nichts sagt, dass die beiden Klassen zusammengehören. Kann die Zeile innerhalb der Bestellung leben?
Die Idee
Verschachtelte Klasse
Ja. Eine Klasse, die innerhalb einer anderen Klasse deklariert ist, heißt verschachtelte Klasse (nested class):
class Order: class Line: def __init__(self, product, quantity, price): self.product = product self.quantity = quantity self.price = price
def total(self): return self.quantity * self.price
def __init__(self): self._lines = []
def add(self, line): self._lines.append(line)
def total(self): total = 0 for line in self._lines: total += line.total() return totalorder = Order()order.add(Order.Line("Kaffeebohnen", 2, 12.5))order.add(Order.Line("Tasse", 4, 3.0))print(f"Gesamt: {order.total()} €")Gesamt: 37.0 €- Von außen heißt sie
Order.Line.Order.Line(...)entsteht ohne einOrder-Objekt, wie bei einer normalen Klasse. - Auch innerhalb der Methoden von
Orderschreibt manOrder.Line. NurLineist dort nicht sichtbar: Die Klasse ist kein Gültigkeitsbereich für ihre Methoden. - Eine verschachtelte Klasse in Python ist an keine bestimmte Bestellung gebunden und sieht deren Attribute nicht. In Java heißt so eine Klasse statisch.
- Heißt sie
_Line, sagt sie nach Konvention „nur für den internen Gebrauch“, wie_lines.
class Order: class Line: pass
def make(self): return Line()
try: Order().make()except NameError as e: print(f"NameError: {e}")print(type(Order.Line()).__qualname__)NameError: name 'Line' is not definedOrder.LineIn Python sind verschachtelte Klassen seltener als in Java. Oft steht die kleine Klasse einfach neben der großen, in derselben Datei, oder statt einer Klasse gibt es ein Dictionary. Darum geht es in der nächsten Karte.
Die Idee
Daten in Daten: Dictionaries und Listen
Hält die Zeile der Bestellung nur Werte und hat keine Methoden, gibt es in Python oft keine Klasse dafür. Die Bestellung ist ein Dictionary, in dem eine Liste von Dictionaries steht:
order = { "customer": "Schmidt", "lines": [ {"product": "Kaffeebohnen", "quantity": 2, "price": 12.5}, {"product": "Tasse", "quantity": 4, "price": 3.0}, ],}
total = 0for line in order["lines"]: total += line["quantity"] * line["price"]print(order["lines"][0]["product"])print(f"Gesamt: {total} €")KaffeebohnenGesamt: 37.0 €order["lines"]ist die Liste,order["lines"][0]ist die erste Zeile, undorder["lines"][0]["product"]ist das Produkt darin. Jedes[...]geht eine Ebene tiefer.- Der Wert in einem Dictionary kann alles sein: eine Zahl, Text, eine Liste, ein anderes Dictionary.
- Eine Klasse ist die bessere Wahl, wenn die Daten Verhalten haben (eine Methode
total) oder Regeln, die eingehalten werden müssen.
Ein Dictionary von Listen sammelt Dinge in Gruppen:
people = [("Jonas", "Berlin"), ("Maria", "Hamburg"), ("Anna", "Berlin")]by_city = {}for name, city in people: if city not in by_city: by_city[city] = [] by_city[city].append(name)print(by_city)print(by_city["Berlin"][1]){'Berlin': ['Jonas', 'Anna'], 'Hamburg': ['Maria']}AnnaGenauer, für Neugierige
- Eine Falle bei verschachtelten Listen:
* 3wiederholt die Referenz, es macht keine drei Listen. Die drei Zeilen sind ein und dieselbe Liste:
grid = [[0] * 3] * 3grid[0][0] = 1print(grid)
grid = []for row in range(3): grid.append([0] * 3)grid[0][0] = 1print(grid)[[1, 0, 0], [1, 0, 0], [1, 0, 0]][[1, 0, 0], [0, 0, 0], [0, 0, 0]]Die Idee
Das verschachtelte Objekt hält das äußere
Der Warenkorb hat einen Rabatt für alle Produkte darin. Jedes Produkt muss den Rabatt seines eigenen Warenkorbs kennen. In Python sieht eine verschachtelte Klasse das äußere Objekt nicht, darum wird der Warenkorb ausdrücklich übergeben:
class Cart: class Item: def __init__(self, cart, name, price): self.cart = cart self.name = name self.price = price
def final_price(self): return self.price * (100 - self.cart._discount_percent) / 100
def __init__(self, discount_percent): self._discount_percent = discount_percent self._items = []
def add(self, name, price): self._items.append(Cart.Item(self, name, price))
def print_items(self): for item in self._items: print(f"{item.name}: {item.final_price()} €")cart = Cart(10)cart.add("Kopfhörer", 80)cart.add("Hülle", 20)cart.print_items()Kopfhörer: 72.0 €Hülle: 18.0 €add übergibt self, und Item hält sie in self.cart. final_price liest den _discount_percent des Warenkorbs, in dem es liegt. Der Unterstrich sagt „intern“, und Item liegt innerhalb von Cart, also ist es erlaubt.cart = Cart(10)cart.add("Kopfhörer", 80)cart.add("Hülle", 20)cart.print_items()Schritt für Schritt, in Worten
- Der Warenkorb ist ein Cart-Objekt mit 10% Rabatt.
- add erzeugt ein Item und übergibt ihm den Warenkorb, self. Neben name und price hält jedes Item in cart einen Pfeil zu seinem Warenkorb.
- Das zweite Item zeigt auf denselben Warenkorb.
- final_price von „Kopfhörer“ folgt dem Pfeil self.cart und liest _discount_percent: 80 * (100 - 10) / 100 = 72.0.
- final_price von „Hülle“ folgt dem Pfeil self.cart und liest _discount_percent: 20 * (100 - 10) / 100 = 18.0.
Genauer, für Neugierige
- Von außen wird ein
Item-Objekt mit dem Warenkorb als erstem Argument erzeugt. Meist erzeugt die äußere Klasse die inneren selbst, wieaddhier:
cart = Cart(10)cable = Cart.Item(cart, "Kabel", 10)print(cable.final_price())9.0- Das Feld
cartist genau das, was in Java das versteckteCart.thismacht. Hier siehst du es einfach. - Benutzt die verschachtelte Klasse die Felder der äußeren nicht, gib ihr das äußere Objekt nicht. So hält sie keine Referenz, die sie nicht braucht.
Probier es aus
Zwei Warenkörbe
Die Klasse Cart ist dieselbe. Ein Warenkorb hat keinen Rabatt, der andere 20 % für Clubmitglieder.
Was gibt das Programm aus?
regular = Cart(0)member = Cart(20)regular.add("Buch", 15)member.add("Buch", 15)regular.print_items()member.print_items()Jedes Item sieht den Rabatt des Warenkorbs, der es erzeugt hat. Das Buch in regular hat 0 %, das in member 20 %.
Probier es aus
Finde den Fehler
print(Order.Line("Kaffee", 12.5).show()) bricht das Programm ab:NameError: name 'currency' is not definedIn welcher Zeile liegt das Problem?
Wähle die Zeile mit dem Fehler.
currency ist ein Attribut eines Order-Objekts, und Line hat keine Bestellung. Einen bloßen Namen currency sucht Python in der Methode und in der ganzen Datei und findet ihn nicht. Der Fehler zeigt sich erst, wenn show aufgerufen wird, nicht früher. Die Lösung: Line bekommt die Bestellung, Order.Line(order, "Kaffee", 12.5), und liest self.order.currency. Ein anderer Weg ist, currency zu einem Klassenattribut zu machen, Order.currency, wenn die Währung für alle Bestellungen dieselbe ist.Die Idee
Eine Klasse in einer Funktion
Eine Klasse kann man auch in einer Funktion deklarieren. Dann ist sie nur dort sichtbar und heißt lokale Klasse:
def print_best_seller(names, sold): class Sale: def __init__(self, name, count): self.name = name self.count = count
best = Sale(names[0], sold[0]) for i in range(1, len(names)): if sold[i] > best.count: best = Sale(names[i], sold[i]) print(f"Bestseller: {best.name} ({best.count} Stk.)")names = ["Kaffee", "Tee", "Wasser"]sold = [120, 45, 300]print_best_seller(names, sold)Bestseller: Wasser (300 Stk.)Sale hält den Namen und die Anzahl zusammen. Außerhalb von print_best_seller benutzt sie niemand, darum steht sie dort. Solche Klassen sind selten, aber es ist gut, sie zu kennen.
Die Idee
Eine Funktion statt einer Klasse
Die Rabatte im Shop können eine abstrakte Klasse mit einer Methode sein, wie im Modul „Vererbung und Polymorphie“:
class Discount(ABC): @abstractmethod def apply(self, price): passUnd für jeden Rabatt eine Klasse: class BlackFriday(Discount): .... In Java gibt es für einen Rabatt, der nur an einer Stelle benutzt wird, die anonyme Klasse: eine Klasse ohne Namen, genau dort geschrieben. Python hat keine anonymen Klassen. Es braucht sie auch nicht: Hat die „Klasse“ eine Methode, übergibt man in Python eine Funktion:
def black_friday(price): return price * 70 / 100
def no_discount(price): return price
def checkout(price, discount): return discount(price)
print(checkout(100, black_friday))print(checkout(100, no_discount))70.0100- Eine Funktion ist ein Wert.
checkout(100, black_friday)übergibt die Funktion, ohne Klammern, undcheckoutruft sie mitdiscount(price)auf. checkoutfragt nicht, wasdiscountist. Es muss sich nur mit einem Preis aufrufen lassen.no_discount(100)gibt die Zahl zurück, die es bekommen hat, darum ist es100und nicht100.0.
Genauer, für Neugierige
- Im Modul „Modernes Python“ siehst du eine kürzere Schreibweise für kleine Funktionen wie
no_discount, direkt an der Stelle, wo sie übergeben werden. - Eine Funktion, die in einer anderen Funktion deklariert ist, sieht deren Variablen. Das macht Funktionen für solche Fälle noch praktischer.
Die Idee
key: Sortieren nach eigener Regel
sorted hat Text und Zahlen sortiert. Schüler kann es aber nicht sortieren:
class Student: def __init__(self, name, grade): self.name = name self.grade = grade
def __repr__(self): return f"{self.name} {self.grade}"students = [ Student("Jonas", 5.50), Student("Maria", 6.00), Student("Peter", 4.75), Student("Anna", 5.50),]sorted(students)TypeError: '<' not supported between instances of 'Student' and 'Student'Python weiß nicht, wonach es Schüler sortieren soll: nach Name, nach Note, nach Alter? < zwischen zwei Student-Objekten ist nicht definiert.
Die Regel gibst du als Funktion in key an. Sie bekommt ein Element und gibt den Wert zurück, nach dem verglichen wird. sorted ruft sie für jeden Schüler auf und sortiert nach dem Ergebnis:
def by_grade(student): return student.grade
print(sorted(students, key=by_grade, reverse=True))[Maria 6.0, Jonas 5.5, Anna 5.5, Peter 4.75]key=by_gradeübergibt die Funktion, ohne Klammern.reverse=Truedreht die Reihenfolge um, darum steht die höchste Note vorn.- Jonas und Anna haben dieselbe Note und bleiben in der Reihenfolge, in der sie waren.
sortedvertauscht gleiche Elemente nicht, auch nicht mitreverse=True. printeiner Liste zeigt die Elemente mit__repr__, darum hatStudentdiese Methode.- Für Text und Zahlen in umgekehrter Reihenfolge reicht
reverse=True:
cities = ["Hamburg", "Berlin", "München"]print(sorted(cities, reverse=True))['München', 'Hamburg', 'Berlin']Genauer, für Neugierige
students.sort(key=by_grade, reverse=True)sortiert die Liste selbst, undsortedgibt eine neue zurück.- Text und Zahlen werden ohne
keysortiert, weil sie<haben. Auch deine Klasse kann eine natürliche Reihenfolge haben, wenn du__lt__schreibst.keyist für die Fälle, in denen es mehrere Reihenfolgen gibt. - Eine Vergleichsfunktion wie
compare(a, b)in Java geht auch, mitfunctools.cmp_to_key. In Python ist aberkeyder übliche Weg: Er ist kürzer und schneller.
Probier es aus
Was steht vorn?
Was gibt das Programm aus?
products = [ Product("Monitor", 220), Product("Maus", 25), Product("Tastatur", 60),]
def by_price(product): return product.price
for p in sorted(products, key=by_price): print(p.name)by_price gibt den Preis zurück, und sorted sortiert vom kleinsten an: 25, 60, 220. Damit das Teuerste vorn steht, füge reverse=True hinzu.Übung
Klassen, die zusammengehören
Löse die Aufgaben in deinem Editor.
1. Warenkorb. Eine Klasse ShoppingCart mit einer verschachtelten Klasse Item (Name und Preis) und den Methoden add_item(item), get_total() und count(). Füge den Artikel ShoppingCart.Item("Milch", 2.5) hinzu, dazu Brot für 1,20 € und Kaffee für 7,50 €. Gib die Summe mit zwei Stellen nach dem Punkt aus.
Artikel: 3Gesamt: 11.20 €2. Monitor für einen Webserver. Eine Klasse WebServer mit einer verschachtelten Klasse Request (Adresse und Statuscode) und den Methoden add_request(r) und print_stats(). Die Statistik sagt, wie viele Anfragen es gibt, wie viele es von jedem Statuscode gibt (sortiert) und wie viel Prozent erfolgreich sind, also mit einem Code von 200 bis 299. Halte die Zähler in einem Dictionary. Die Anfragen sind /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Anfragen: 5200: 3401: 1500: 1Erfolgreich: 60%3. Hausverwaltung. Eine Klasse Building mit einer verschachtelten Klasse Apartment (Nummer, Anzahl der Zimmer, Eigentümer) und den Methoden add_apartment(a) und get_apartment_info(number). Halte die Wohnungen in einem Dictionary von der Nummer zur Wohnung. Füge 101 mit 3 Zimmern von Schmidt und 102 mit 2 Zimmern von Müller hinzu. Suche 101 und 999.
Wohnung 101: 3 Zimmer, Eigentümer SchmidtWohnung 999 gibt es nicht.4. Rangliste. Sortiere die Schüler aus der Lektion nach Note, von der höchsten abwärts. Bei gleicher Note sortiere sie nach Namen, alphabetisch. Alles steckt in einer key-Funktion: Lass sie ein Tupel (-Note, Name) zurückgeben. Tupel werden nach dem ersten Element verglichen und, wenn das gleich ist, nach dem zweiten. Das Minus dreht nur die Reihenfolge der Note um.
Maria 6.0Anna 5.5Jonas 5.5Peter 4.75Weitere Aufgaben
- Computer-Konfigurator. Eine Klasse
Computermit den verschachtelten KlassenCPU(Marke, Takt in GHz),RAM(GB) undStorage(GB) und den Methodenset_cpu,set_ram,set_storage.__str__beschreibt die Konfiguration:Intel 3.5 GHz, 16 GB RAM, 512 GB. - Reiseplaner, noch einmal. Löse die Aufgabe aus der Lektion über Dictionaries einmal mit einer verschachtelten Klasse
TripPlanner.Destination, die den Namen und die Liste der Aktivitäten hält, und einmal nur mit einem Dictionary von Listen. Was ist praktischer? - Kontaktnetz, noch einmal. Verschiebe
Contactaus der Aufgabe in der Lektion über Dictionaries inContactNetwork. SollteContactsein Netz kennen? - Kopfrechnen, noch einmal. Verschiebe
Questionaus der Aufgabe in der Lektion über Listen inMathGame. - Produkte auf zwei Arten. Sortiere eine Liste von Produkten nach Preis, vom teuersten an, und bei gleichem Preis nach Namen. Sortiere dann dieselbe Liste nur nach Namen, in umgekehrter Reihenfolge.
- Dein eigener Rabatt. Schreib drei Rabattfunktionen: 10 % ab 100 €, feste 5 € Rabatt und „zweites Produkt zum halben Preis“. Wende sie mit ein und derselben
checkoutauf ein und denselben Warenkorb an.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Strings · Listen: append, remove und sort · Stack, Queue und verkettete Liste · dict und set
- Geht weiter in: Lambdas · List Comprehensions und Generatoren · Entwurfsmuster · Tests mit unittest
- Aufgaben zum Thema: Collections