Aufwärmen · Aufgabe 1 von 7
// A1.2 · ca. 30 Min. · Vertiefung
Operatoren überladen
Nach dieser Lektion lassen Sie +, * und die Vergleichsoperatoren mit eigenen Klassen arbeiten, auch mit sum(), 3 * x und sorted().
Danach können Sie
- __add__, __radd__ und __mul__ schreiben und für nicht unterstützte Operanden NotImplemented liefern
- Vorhersagen, wann Python eine gespiegelte Methode wie __radd__ aufruft und was += tut
- Ordnung mit Vergleichsmethoden oder mit functools.total_ordering festlegen
Vorhersagen · Aufgabe 2 von 7
Sagen Sie es vorher, bevor Sie weiterlesen: Money unterstützt Money + Money. Was passiert hier?
class Money: def __init__(self, cents): self.cents = cents def __add__(self, other): if not isinstance(other, Money): return NotImplemented return Money(self.cents + other.cents) total = sum([Money(100), Money(250)]) print(total.cents)Üben · Aufgabe 3 von 7
Money(5) * 3 funktioniert schon. Nennen Sie die Methode, mit der auch 3 * Money(5) funktioniert.
class Money: def __init__(self, cents): self.cents = cents def __mul__(self, factor): if not isinstance(factor, int): return NotImplemented return Money(self.cents * factor) def ____(self, factor): return self.__mul__(factor)def (self, factor):Üben · Aufgabe 4 von 7
Money definiert nur __lt__. Was gibt das aus?
class Money: def __init__(self, cents): self.cents = cents def __lt__(self, other): return self.cents < other.cents print(Money(5) > Money(3))Üben · Aufgabe 5 von 7
a und b sind Objekte einer Klasse mit __add__, __radd__ und __lt__, sonst nichts. Ordnen Sie jedem Ausdruck den Aufruf zu, den Python macht.
Denksport · Aufgabe 6 von 7
Knobelaufgabe. total_ordering ergänzt die fehlenden Vergleiche. Was gibt das aus?
from functools import total_ordering @total_ordering class Money: def __init__(self, cents): self.cents = cents def __lt__(self, other): return self.cents < other.cents print(Money(5) <= Money(5))Anwenden · Aufgabe 7 von 7
Mini-Aufgabe. Schreiben Sie eine Klasse Distance(metres), deren Objekte sich mit + addieren, mit sum() summieren, mit sorted() sortieren und mit allen vier Ordnungsoperatoren vergleichen lassen. Verwenden Sie total_ordering. Probieren Sie sum, sorted und max mit Distance(1200), Distance(800) und Distance(450).
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Selbst programmieren
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Ein Geldtyp mit Rechnen und Ordnung
Money addiert sich mit + und sum(), multipliziert mit ganzen Zahlen von beiden Seiten und ordnet sich über total_ordering. Verschiedene Währungen weist es mit ValueError ab, denn das ist ein falscher Wert. Bei einem float antwortet es mit NotImplemented, und Python löst den üblichen TypeError aus. Ändern Sie den Warenkorb oder probieren Sie Money(1) * 1.5.
main.py
from functools import total_ordering
@total_ordering
class Money:
"""An amount in cents of one currency."""
def __init__(self, cents: int, currency: str = "EUR") -> None:
self.cents = cents
self.currency = currency
def __repr__(self) -> str:
return f"Money({self.cents}, {self.currency!r})"
def _check(self, other: "Money") -> None:
if other.currency != self.currency:
raise ValueError(f"cannot mix {self.currency} and {other.currency}")
def __add__(self, other: object) -> "Money":
if isinstance(other, int) and other == 0: # sum() starts with 0
return self
if not isinstance(other, Money):
return NotImplemented
self._check(other)
return Money(self.cents + other.cents, self.currency)
__radd__ = __add__
def __mul__(self, factor: object) -> "Money":
if not isinstance(factor, int):
return NotImplemented
return Money(self.cents * factor, self.currency)
__rmul__ = __mul__
def __eq__(self, other: object) -> bool:
if not isinstance(other, Money):
return NotImplemented
return (self.cents, self.currency) == (other.cents, other.currency)
def __lt__(self, other: "Money") -> bool:
if not isinstance(other, Money):
return NotImplemented
self._check(other)
return self.cents < other.cents
def __hash__(self) -> int:
return hash((self.cents, self.currency))
basket = [Money(450), Money(120), Money(999)]
print("Total:", sum(basket))
print("Three coffees:", 3 * Money(320))
print("Cheapest first:", sorted(basket))
print("Over 5 EUR:", [m for m in basket if m >= Money(500)])
try:
Money(100) + 2.5
except TypeError as err:
print("TypeError:", err)
try:
Money(100) < Money(100, "USD")
except ValueError as err:
print("ValueError:", err)
Ausführen mit
python main.pyAusgabe
Total: Money(1569, 'EUR')
Three coffees: Money(960, 'EUR')
Cheapest first: [Money(120, 'EUR'), Money(450, 'EUR'), Money(999, 'EUR')]
Over 5 EUR: [Money(999, 'EUR')]
TypeError: unsupported operand type(s) for +: 'Money' and 'float'
ValueError: cannot mix EUR and USD- sum() funktioniert, weil __radd__ = __add__ die Start-0 annimmt.
- 3 * Money(320) landet bei __rmul__, weil int.__mul__ für Money NotImplemented liefert.
- m >= Money(500) stammt von total_ordering: Die Klasse schreibt nur __eq__ und __lt__.
- Money + 2.5 liefert NotImplemented, float hat auch keine Antwort, also löst Python TypeError aus.
Ändern und ausführen
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Übungen
Übung 1 von 2
Dauern addieren
Bringen Sie Duration in main.py bei: Duration + Duration, sum() über eine Liste von Dauern und Multiplikation mit einem int von beiden Seiten, also Duration(15) * 3 und 3 * Duration(15). Geben Sie immer eine neue Duration zurück. Für jeden anderen Operanden liefern Sie NotImplemented, sodass Duration(5) + "x" TypeError auslöst.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
sum() rechnet 0 + erstes Element, __radd__ muss also das int 0 annehmen.
Hinweis 2
Behandeln Sie in __add__ eine Duration, dann das int 0 (return self), und liefern Sie für alles andere NotImplemented. Schreiben Sie dann __radd__ = __add__.
Hinweis 3
__mul__ prüft isinstance(factor, int) und gibt Duration(self.minutes * factor) zurück; __rmul__ = __mul__ deckt 3 * Duration(15) ab.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
class Duration:
"""A length of time in whole minutes."""
def __init__(self, minutes: int) -> None:
self.minutes = minutes
def __repr__(self) -> str:
return f"Duration({self.minutes})"
def __add__(self, other: object) -> "Duration":
if isinstance(other, Duration):
return Duration(self.minutes + other.minutes)
if isinstance(other, int) and other == 0:
return self
return NotImplemented
__radd__ = __add__
def __mul__(self, factor: object) -> "Duration":
if not isinstance(factor, int):
return NotImplemented
return Duration(self.minutes * factor)
__rmul__ = __mul__
if __name__ == "__main__":
print(sum([Duration(30), Duration(45)]), 2 * Duration(15))
Auf dem eigenen Computer ausführen
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
class Duration:
"""A length of time in whole minutes."""
def __init__(self, minutes: int) -> None:
self.minutes = minutes
def __repr__(self) -> str:
return f"Duration({self.minutes})"
# Add __add__ (Duration + Duration, and Duration + 0 for sum()),
# __radd__, __mul__ (Duration * int) and __rmul__ (int * Duration).
# Return NotImplemented for any other operand.
if __name__ == "__main__":
print(sum([Duration(30), Duration(45)]), 2 * Duration(15))
test_main.py
from main import Duration
def test_add():
"""Duration + Duration addiert die Minuten"""
got = (Duration(30) + Duration(45)).minutes
assert got == 75, f"Duration(30) + Duration(45) hat {got} Minuten, erwartet: 75"
def test_sum():
"""sum() über Dauern funktioniert"""
got = sum([Duration(10), Duration(20), Duration(30)]).minutes
assert got == 60, f"sum() ergab {got} Minuten, erwartet: 60; sum() beginnt mit 0 + Duration(10)"
def test_multiply_both_sides():
"""Duration * 3 und 3 * Duration funktionieren beide"""
got = (Duration(15) * 3).minutes, (3 * Duration(15)).minutes
assert got == (45, 45), f"ergab {got!r} Minuten, erwartet: (45, 45)"
def test_other_types():
"""Einen String zu addieren löst TypeError aus, weil __add__ NotImplemented liefert"""
assert Duration(5).__add__("x") is NotImplemented, "Duration(5).__add__('x') sollte NotImplemented liefern"
try:
Duration(5) + "x"
except TypeError:
return
raise AssertionError("Duration(5) + 'x' hat keinen TypeError ausgelöst")
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 2 von 2
Versionsnummern ordnen
Version(1, 10, 0) muss nach Version(1, 2, 3) einsortiert werden. Dekorieren Sie die Klasse mit @total_ordering und ergänzen Sie __eq__ und __lt__, die (major, minor, patch) vergleichen. Beide liefern für andere Typen NotImplemented, sodass Version(1, 0, 0) < "2" TypeError auslöst. Ergänzen Sie ein passendes __hash__, da Sie __eq__ definieren.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
Tupel werden Element für Element verglichen, also ist (1, 2, 3) < (1, 10, 0) True, anders als bei den Strings "1.2.3" und "1.10.0".
Hinweis 2
Eine Hilfsmethode def _key(self), die (self.major, self.minor, self.patch) liefert, hält __eq__, __lt__ und __hash__ im Gleichschritt.
Hinweis 3
Setzen Sie @total_ordering in die Zeile über class Version. Es leitet dann <=, > und >= aus __eq__ und __lt__ ab.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
from functools import total_ordering
@total_ordering
class Version:
"""A version number such as 1.10.2, ordered part by part."""
def __init__(self, major: int, minor: int, patch: int) -> None:
self.major = major
self.minor = minor
self.patch = patch
def __repr__(self) -> str:
return f"Version({self.major}, {self.minor}, {self.patch})"
def _key(self) -> tuple[int, int, int]:
return (self.major, self.minor, self.patch)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return self._key() == other._key()
def __lt__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return self._key() < other._key()
def __hash__(self) -> int:
return hash(self._key())
if __name__ == "__main__":
print(sorted([Version(1, 10, 0), Version(1, 2, 3)]))
Auf dem eigenen Computer ausführen
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
from functools import total_ordering
class Version:
"""A version number such as 1.10.2, ordered part by part."""
def __init__(self, major: int, minor: int, patch: int) -> None:
self.major = major
self.minor = minor
self.patch = patch
def __repr__(self) -> str:
return f"Version({self.major}, {self.minor}, {self.patch})"
# Decorate the class with @total_ordering, then add __eq__ and __lt__.
# Both return NotImplemented when other is not a Version.
if __name__ == "__main__":
print(sorted([Version(1, 10, 0), Version(1, 2, 3)]))
test_main.py
from main import Version
def test_sorted():
"""1.2.3 kommt vor 1.10.0, anders als in der String-Reihenfolge"""
got = sorted([Version(1, 10, 0), Version(1, 2, 3), Version(0, 9, 9)])
want = [Version(0, 9, 9), Version(1, 2, 3), Version(1, 10, 0)]
assert got == want, f"sorted() ergab {got!r}, erwartet: {want!r}"
def test_all_four_orderings():
"""<, <=, > und >= funktionieren alle"""
a, b = Version(1, 2, 3), Version(1, 2, 4)
got = a < b, a <= b, b > a, b >= a, a <= Version(1, 2, 3)
assert got == (True, True, True, True, True), f"die Vergleiche ergaben {got!r}, alle sollten True sein"
def test_equality():
"""Gleiche Teile heißen gleiche Versionen; ein String ist nie gleich"""
assert Version(2, 0, 0) == Version(2, 0, 0), "Version(2, 0, 0) == Version(2, 0, 0) ist False"
assert Version(2, 0, 0) != "2.0.0", "eine Version darf nicht gleich dem String '2.0.0' sein"
def test_other_types():
"""Ein Ordnungsvergleich mit einem String löst TypeError aus"""
try:
Version(1, 0, 0) < "2"
except TypeError:
return
raise AssertionError("Version(1, 0, 0) < '2' hat keinen TypeError ausgelöst: Liefern Sie für andere Typen NotImplemented")
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenHäufige Fehler
sum() ohne __radd__
class Money:
def __init__(self, cents):
self.cents = cents
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
return Money(self.cents + other.cents)
total = sum([Money(100), Money(250)])
Was Python ausgibt
TypeError: unsupported operand type(s) for +: 'int' and 'Money'Warum, und die Lösung
sum() beginnt mit 0, und 0 + Money(100) braucht Money.__radd__. Nehmen Sie 0 in __add__ an und ergänzen Sie __radd__ = __add__, oder geben Sie sum() einen Startwert: sum(items, Money(0)).
total_ordering ohne Ordnungsmethode
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major):
self.major = major
def __eq__(self, other):
return self.major == other.major
Was Python ausgibt
ValueError: must define at least one ordering operation: < > <= >=Warum, und die Lösung
total_ordering leitet die fehlenden Vergleiche aus einem vorhandenen ab. __eq__ zählt nicht; definieren Sie zusätzlich __lt__, __le__, __gt__ oder __ge__. Der Fehler erscheint schon beim Erzeugen der Klasse, bevor es ein Objekt gibt.
__add__, das self ändert und nichts zurückgibt
class Money:
def __init__(self, cents):
self.cents = cents
def __add__(self, other):
self.cents += other.cents
total = Money(100) + Money(250)
print(total.cents)
Was Python ausgibt
AttributeError: 'NoneType' object has no attribute 'cents'Warum, und die Lösung
Eine Operatormethode muss ihr Ergebnis zurückgeben; ohne return ergibt x + y den Wert None. Sie sollte außerdem beide Operanden unverändert lassen: return Money(self.cents + other.cents). self direkt zu ändern ist Aufgabe von __iadd__, das += bedient.
Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode
Abschlussquiz
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.