Zum Inhalt springen
aviral gupta

// 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().

Lektion 2 von 6 in A1 Das Datenmodell

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
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Lektion A1.1: __eq__ liefert NotImplemented, wenn das andere Objekt nicht seinen Typ hat. Was macht a == b danach?

  2. 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)
  3. Ü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):
  4. Ü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))
  5. Ü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.

  6. 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))
  7. 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.py

Ausgabe

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
  1. Hinweis 1

    sum() rechnet 0 + erstes Element, __radd__ muss also das int 0 annehmen.

  2. 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__.

  3. 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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.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
  1. 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".

  2. Hinweis 2

    Eine Hilfsmethode def _key(self), die (self.major, self.minor, self.patch) liefert, hält __eq__, __lt__ und __hash__ im Gleichschritt.

  3. 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.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Hä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.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Binäre Operatoren und NotImplemented

x + y ruft type(x).__add__(x, y) auf; * ruft __mul__ auf, - ruft __sub__ auf. Geben Sie ein neues Objekt zurück und lassen Sie beide Operanden unverändert. Hat der andere Operand einen Typ, den Sie nicht unterstützen, liefern Sie NotImplemented; lösen Sie keinen Fehler aus und liefern Sie nicht False. Python fragt dann den anderen Operanden und löst, wenn niemand zuständig ist, den bekannten TypeError aus: unsupported operand type(s) for +.

Gespiegelte Methoden und +=

Fehlt x.__add__(y) oder liefert es NotImplemented, und hat y einen anderen Typ, ruft Python y.__radd__(x) auf. So funktionieren 3 * money und sum(): sum() beginnt mit 0, der erste Schritt ist also 0 + element, und int kann Ihre Klasse nicht addieren. Für eine kommutative Operation genügt __radd__ = __add__ im Klassenrumpf. x += y ruft __iadd__ auf, falls vorhanden; sonst gilt x = x + y, und x wird an ein neues Objekt gebunden.

Vergleichsmethoden und total_ordering

<, <=, >, >= rufen __lt__, __le__, __gt__, __ge__ auf. __lt__ und __gt__ sind gegenseitig gespiegelt, also funktioniert a > b auch nur mit __lt__, über b < a. Mehr leitet Python nicht ab: <= entsteht nicht aus < und ==. functools.total_ordering ergänzt den Rest aus __eq__ plus einer Ordnungsmethode. Fehlt __eq__, fällt das abgeleitete <= auf die Identität zurück, und Money(5) <= Money(5) ist False.

Quellen

Zuletzt geprüft am 29. September 2026