Zum Inhalt
wannacode

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 total
order = Order()
order.add(Order.Line("Kaffeebohnen", 2, 12.5))
order.add(Order.Line("Tasse", 4, 3.0))
print(f"Gesamt: {order.total()} €")
Ausgabe
Gesamt: 37.0 €
  • Von außen heißt sie Order.Line. Order.Line(...) entsteht ohne ein Order-Objekt, wie bei einer normalen Klasse.
  • Auch innerhalb der Methoden von Order schreibt man Order.Line. Nur Line ist 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__)
Ausgabe
NameError: name 'Line' is not defined
Order.Line

In 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 = 0
for line in order["lines"]:
total += line["quantity"] * line["price"]
print(order["lines"][0]["product"])
print(f"Gesamt: {total} €")
Ausgabe
Kaffeebohnen
Gesamt: 37.0 €
  • order["lines"] ist die Liste, order["lines"][0] ist die erste Zeile, und order["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])
Ausgabe
{'Berlin': ['Jonas', 'Anna'], 'Hamburg': ['Maria']}
Anna
Genauer, für Neugierige
  • Eine Falle bei verschachtelten Listen: * 3 wiederholt die Referenz, es macht keine drei Listen. Die drei Zeilen sind ein und dieselbe Liste:
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)
grid = []
for row in range(3):
grid.append([0] * 3)
grid[0][0] = 1
print(grid)
Ausgabe
[[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()
Ausgabe
Kopfhörer: 72.0 €
Hülle: 18.0 €
Java hat eine innere Klasse (inner class): Jedes Objekt davon hält eine versteckte Referenz auf das äußere Objekt. Python hat so etwas nicht, darum ist die Referenz sichtbar: 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
  1. Der Warenkorb ist ein Cart-Objekt mit 10% Rabatt.
  2. 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.
  3. Das zweite Item zeigt auf denselben Warenkorb.
  4. final_price von „Kopfhörer“ folgt dem Pfeil self.cart und liest _discount_percent: 80 * (100 - 10) / 100 = 72.0.
  5. 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, wie add hier:
cart = Cart(10)
cable = Cart.Item(cart, "Kabel", 10)
print(cable.final_price())
Ausgabe
9.0
  • Das Feld cart ist genau das, was in Java das versteckte Cart.this macht. 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()
Antwort

Probier es aus

Finde den Fehler

Die Zeile soll die Währung der Bestellung zeigen. Bei print(Order.Line("Kaffee", 12.5).show()) bricht das Programm ab:
Fehler
NameError: name 'currency' is not defined

In welcher Zeile liegt das Problem?

Wähle die Zeile mit dem Fehler.

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)
Ausgabe
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):
pass

Und 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))
Ausgabe
70.0
100
  • Eine Funktion ist ein Wert. checkout(100, black_friday) übergibt die Funktion, ohne Klammern, und checkout ruft sie mit discount(price) auf.
  • checkout fragt nicht, was discount ist. Es muss sich nur mit einem Preis aufrufen lassen.
  • no_discount(100) gibt die Zahl zurück, die es bekommen hat, darum ist es 100 und nicht 100.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)
Fehler
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))
Ausgabe
[Maria 6.0, Jonas 5.5, Anna 5.5, Peter 4.75]
  • key=by_grade übergibt die Funktion, ohne Klammern. reverse=True dreht 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. sorted vertauscht gleiche Elemente nicht, auch nicht mit reverse=True.
  • print einer Liste zeigt die Elemente mit __repr__, darum hat Student diese Methode.
  • Für Text und Zahlen in umgekehrter Reihenfolge reicht reverse=True:
cities = ["Hamburg", "Berlin", "München"]
print(sorted(cities, reverse=True))
Ausgabe
['München', 'Hamburg', 'Berlin']
Genauer, für Neugierige
  • students.sort(key=by_grade, reverse=True) sortiert die Liste selbst, und sorted gibt eine neue zurück.
  • Text und Zahlen werden ohne key sortiert, weil sie < haben. Auch deine Klasse kann eine natürliche Reihenfolge haben, wenn du __lt__ schreibst. key ist für die Fälle, in denen es mehrere Reihenfolgen gibt.
  • Eine Vergleichsfunktion wie compare(a, b) in Java geht auch, mit functools.cmp_to_key. In Python ist aber key der ü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)
Antwort

Ü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.

Ausgabe
Artikel: 3
Gesamt: 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.

Ausgabe
Anfragen: 5
200: 3
401: 1
500: 1
Erfolgreich: 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.

Ausgabe
Wohnung 101: 3 Zimmer, Eigentümer Schmidt
Wohnung 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.

Ausgabe
Maria 6.0
Anna 5.5
Jonas 5.5
Peter 4.75

Weitere Aufgaben

  • Computer-Konfigurator. Eine Klasse Computer mit den verschachtelten Klassen CPU (Marke, Takt in GHz), RAM (GB) und Storage (GB) und den Methoden set_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 Contact aus der Aufgabe in der Lektion über Dictionaries in ContactNetwork. Sollte Contact sein Netz kennen?
  • Kopfrechnen, noch einmal. Verschiebe Question aus der Aufgabe in der Lektion über Listen in MathGame.
  • 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 checkout auf ein und denselben Warenkorb an.

Test

Teste dich

1. class Line steht innerhalb von class Order. Sieht Line die Attribute einer bestimmten Bestellung?
2. Warum bricht show() von Order.Line mit NameError: name 'currency' is not defined ab?
3. Was benutzt Python statt einer anonymen Klasse mit einer Methode, wie beim Rabatt?
4. Was macht sorted(students, key=by_grade)?
5. Was sortiert die Schüler nach Note, von der höchsten abwärts?
6. grid = [[0] * 3] * 3 und danach grid[0][0] = 1. Was steht in grid?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·