Zum Inhalt springen
aviral gupta

// A1.1 · ca. 30 Min. · Vertiefung

__repr__, __str__, __eq__ und __hash__

Nach dieser Lektion geben Sie einer Klasse ein hilfreiches repr und str, vergleichen ihre Objekte nach Wert und machen sie zu sicheren Set-Elementen und Dict-Schlüsseln.

Lektion 1 von 6 in A1 Das Datenmodell

Anfang des Moduls

Danach können Sie

  • __repr__ und __str__ schreiben und vorhersagen, welche Methode print, f-Strings und Container nutzen
  • __eq__ und __hash__ so schreiben, dass gleiche Objekte gleiche Hashes haben
  • Erklären, warum eine Klasse mit nur __eq__ unhashbar ist und warum sich ein Hash nie ändern darf
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Modul I2: Welche Methoden schreibt ein einfaches @dataclass (ohne Argumente) für Sie? Wählen Sie alle zutreffenden.

    Wählen Sie alle zutreffenden aus.

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: Die Klasse definiert __repr__ und __str__. Was gibt das aus?

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __repr__(self):
            return f"Point({self.x}, {self.y})"
    
        def __str__(self):
            return f"({self.x}, {self.y})"
    
    
    p = Point(1, 2)
    print(p, [p])
  3. Üben · Aufgabe 3 von 7

    Setzen Sie die eingebaute Funktion ein, damit der Hash aus denselben Feldern entsteht, die __eq__ vergleicht.

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __eq__(self, other):
            if not isinstance(other, Point):
                return NotImplemented
            return (self.x, self.y) == (other.x, other.y)
    
        def __hash__(self):
            return ____((self.x, self.y))
    return ((self.x, self.y))
  4. Üben · Aufgabe 4 von 7

    Money definiert __eq__, aber kein __hash__. Was gibt das aus?

    class Money:
        def __init__(self, cents):
            self.cents = cents
    
        def __eq__(self, other):
            return isinstance(other, Money) and self.cents == other.cents
    
    
    print(Money(5) == Money(5), Money.__hash__)
  5. Üben · Aufgabe 5 von 7

    Die Klasse definiert __repr__ und __str__. Ordnen Sie jedem Ausdruck die Methode zu, die seinen Text liefert.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Das Ticket kommt in ein Set, danach ändert sich seine Nummer. Was gibt das aus?

    class Ticket:
        def __init__(self, number):
            self.number = number
    
        def __eq__(self, other):
            return isinstance(other, Ticket) and self.number == other.number
    
        def __hash__(self):
            return hash(self.number)
    
    
    t = Ticket(1)
    open_tickets = {t}
    t.number = 2
    print(t in open_tickets, Ticket(1) in open_tickets)
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie eine Klasse Money(cents, currency). repr liefert Money(250, 'EUR'), str liefert 2.50 EUR, zwei Objekte sind gleich, wenn cents und currency übereinstimmen, und ein Set aus Money(250, "EUR"), Money(250, "EUR") und Money(250, "USD") hat zwei Elemente.

    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

Artikelnummern als Dict-Schlüssel

Ein Laden scannt Artikel als Produktcode plus Größe. Codes kommen in beliebiger Schreibweise, also speichert Sku sie in Großbuchstaben und vergleicht nach Wert. Weil __eq__ und __hash__ dieselben zwei Felder nutzen, fällt der doppelte Scan im Set zusammen, und eine neu gebaute Sku findet ihren Bestand im Dict. In der Liste erscheint repr, in den Berichtszeilen str.

main.py

class Sku:
    """A stock-keeping unit: product code plus size, compared by value."""

    def __init__(self, code: str, size: str) -> None:
        self.code = code.upper()
        self.size = size

    def __repr__(self) -> str:
        return f"Sku({self.code!r}, {self.size!r})"

    def __str__(self) -> str:
        return f"{self.code}-{self.size}"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Sku):
            return NotImplemented
        return (self.code, self.size) == (other.code, other.size)

    def __hash__(self) -> int:
        return hash((self.code, self.size))


scanned = [Sku("tee", "M"), Sku("TEE", "M"), Sku("cap", "L")]
print("Scanned:", scanned)
print("Unique items:", len(set(scanned)))

stock = {Sku("TEE", "M"): 12, Sku("CAP", "L"): 3}
for sku in scanned:
    print(f"{sku}: {stock[sku]} in stock")

print(Sku("tee", "M") == "TEE-M")

Ausführen mit

python main.py

Ausgabe

Scanned: [Sku('TEE', 'M'), Sku('TEE', 'M'), Sku('CAP', 'L')]
Unique items: 2
TEE-M: 12 in stock
TEE-M: 12 in stock
CAP-L: 3 in stock
False
  • Die Liste zeigt jede Sku mit __repr__; die f-String-Zeilen verwenden __str__.
  • Sku("tee", "M") und Sku("TEE", "M") sind gleich und haben denselben Hash, also behält das Set eine.
  • stock[sku] klappt mit einem anderen Objekt als dem gespeicherten Schlüssel, weil Hash und == zusammenpassen.
  • Beim Vergleich mit einem String liefert __eq__ NotImplemented, und Python fällt auf die Identität zurück: False.
Ä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

Zwei Gesichter einer Farbe

Geben Sie der Klasse Color in main.py ein __repr__ und ein __str__. repr(Color(255, 128, 0)) muss Color(255, 128, 0) sein und str() der Hex-Code #ff8000, mit zwei kleingeschriebenen Hex-Ziffern je Kanal.

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

    Beide Methoden bekommen nur self und müssen einen str zurückgeben, nicht ausgeben.

  2. Hinweis 2

    Die Formatangabe 02x schreibt ein int als zwei kleine Hex-Ziffern: f"{10:02x}" ergibt 0a.

  3. Hinweis 3

    return f"#{self.red:02x}{self.green:02x}{self.blue:02x}" für __str__ und f"Color({self.red}, {self.green}, {self.blue})" für __repr__.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

class Color:
    """An RGB colour with channels from 0 to 255."""

    def __init__(self, red: int, green: int, blue: int) -> None:
        self.red = red
        self.green = green
        self.blue = blue

    def __repr__(self) -> str:
        return f"Color({self.red}, {self.green}, {self.blue})"

    def __str__(self) -> str:
        return f"#{self.red:02x}{self.green:02x}{self.blue:02x}"


if __name__ == "__main__":
    orange = Color(255, 128, 0)
    print(repr(orange), orange)
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 Color:
    """An RGB colour with channels from 0 to 255."""

    def __init__(self, red: int, green: int, blue: int) -> None:
        self.red = red
        self.green = green
        self.blue = blue

    # Add __repr__: Color(255, 128, 0)
    # Add __str__: #ff8000


if __name__ == "__main__":
    orange = Color(255, 128, 0)
    print(repr(orange), orange)

test_main.py

from main import Color


def test_repr():
    """repr() sieht aus wie der Aufruf, der die Farbe baut"""
    got = repr(Color(255, 128, 0))
    assert got == "Color(255, 128, 0)", f"repr(Color(255, 128, 0)) lieferte {got!r}, erwartet: 'Color(255, 128, 0)'"


def test_str():
    """str() ist der Hex-Code mit zwei Ziffern je Kanal"""
    got = str(Color(255, 128, 0)), str(Color(0, 10, 255))
    assert got == ("#ff8000", "#000aff"), f"str() lieferte {got!r}, erwartet: ('#ff8000', '#000aff')"


def test_print_uses_str():
    """Ein f-String verwendet __str__"""
    got = f"colour: {Color(0, 0, 0)}"
    assert got == "colour: #000000", f"der f-String ergab {got!r}, erwartet: 'colour: #000000'"


def test_list_uses_repr():
    """Eine Liste zeigt ihre Elemente mit __repr__"""
    got = str([Color(1, 2, 3)])
    assert got == "[Color(1, 2, 3)]", f"str([Color(1, 2, 3)]) lieferte {got!r}, erwartet: '[Color(1, 2, 3)]'"

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

Punkte in einem Set

Point in main.py hat ein __repr__. Ergänzen Sie __eq__ und __hash__, sodass Punkte mit gleichem x und y gleich sind, ein Set nur einen davon behält und ein gleicher Punkt einen Dict-Eintrag findet. Für alles, was kein Point ist, muss __eq__ NotImplemented liefern.

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

    Ohne __eq__ sind zwei Point(1, 2) verschieden, weil == auf die Identität zurückfällt.

  2. Hinweis 2

    Prüfen Sie in __eq__ zuerst isinstance(other, Point) und liefern Sie sonst NotImplemented.

  3. Hinweis 3

    Vergleichen Sie (self.x, self.y) == (other.x, other.y) und geben Sie in __hash__ hash((self.x, self.y)) zurück.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

class Point:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"Point({self.x}, {self.y})"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self) -> int:
        return hash((self.x, self.y))


if __name__ == "__main__":
    print({Point(1, 2), Point(1, 2)})
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 Point:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"Point({self.x}, {self.y})"

    # Add __eq__ (NotImplemented for other types) and a matching __hash__.


if __name__ == "__main__":
    print({Point(1, 2), Point(1, 2)})

test_main.py

from main import Point


def test_equal_points():
    """Zwei Punkte mit denselben Koordinaten sind gleich"""
    assert Point(1, 2) == Point(1, 2), "Point(1, 2) == Point(1, 2) ist False: Vergleichen Sie x und y in __eq__"
    assert Point(1, 2) != Point(2, 1), "Point(1, 2) != Point(2, 1) ist False"


def test_set_removes_duplicates():
    """Ein Set behält einen von zwei gleichen Punkten"""
    got = len({Point(1, 2), Point(1, 2), Point(3, 4)})
    assert got == 2, f"das Set enthält {got} Punkte, erwartet: 2"


def test_dict_key():
    """Ein gleicher Punkt findet den Dict-Eintrag"""
    names = {Point(0, 0): "origin"}
    got = names.get(Point(0, 0))
    assert got == "origin", f"names.get(Point(0, 0)) lieferte {got!r}, erwartet: 'origin'"


def test_other_types():
    """__eq__ liefert NotImplemented für alles, was kein Point ist"""
    got = Point(1, 2).__eq__((1, 2))
    assert got is NotImplemented, f"Point(1, 2).__eq__((1, 2)) lieferte {got!r}, erwartet: NotImplemented"
    assert Point(1, 2) != (1, 2), "Point(1, 2) darf nicht gleich dem Tupel (1, 2) sein"

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

__eq__ definieren und __hash__ vergessen

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)


seen = {Point(1, 2)}

Was Python ausgibt

TypeError: cannot use 'Point' as a set element (unhashable type: 'Point')

Warum, und die Lösung

Wer __eq__ überschreibt, setzt __hash__ auf None, und die Objekte können weder in ein Set noch Dict-Schlüssel sein. Ergänzen Sie def __hash__(self): return hash((self.x, self.y)) mit denselben Feldern, die __eq__ vergleicht. Sind die Objekte veränderlich, lassen Sie sie bewusst unhashbar.

__repr__, das keinen str zurückgibt

class Order:
    def __init__(self, number):
        self.number = number

    def __repr__(self):
        return self.number


print(repr(Order(42)))

Was Python ausgibt

TypeError: __repr__ returned non-string (type int)

Warum, und die Lösung

__repr__ und __str__ müssen einen str liefern; Python prüft den Typ und löst sonst einen Fehler aus. Bauen Sie den Text: return f"Order({self.number!r})". Dasselbe gilt für __hash__, das ein int liefern muss.

__eq__, das denselben Typ beim anderen Objekt voraussetzt

class Point:
    def __init__(self, x):
        self.x = x

    def __eq__(self, other):
        return self.x == other.x


print(Point(1) == "a")

Was Python ausgibt

AttributeError: 'str' object has no attribute 'x'

Warum, und die Lösung

Auf der anderen Seite von == kann jedes Objekt landen, etwa wenn eine Liste mit in durchsucht wird. Prüfen Sie zuerst den Typ und liefern Sie für fremde Objekte NotImplemented: if not isinstance(other, Point): return NotImplemented. Python fragt dann den anderen Operanden und antwortet zuletzt False.

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

repr für Entwickler, str für Nutzer

repr(obj) ruft __repr__ auf. Das Ergebnis soll eindeutig sein und möglichst wie der Ausdruck aussehen, der das Objekt neu baut: Point(1, 2). str(obj), print() und f"{obj}" rufen __str__ auf, eine lesbare Form wie (1, 2). Definiert eine Klasse nur __repr__, greift str() darauf zurück. Umgekehrt gilt das nicht: Container zeigen ihre Elemente mit repr, also gibt print([p]) [Point(1, 2)] aus, auch wenn __str__ existiert. Im f-String fordert !r das repr an.

Gleichheit und der Hash-Vertrag

Standardmäßig vergleicht == die Identität. Definieren Sie __eq__, um nach Wert zu vergleichen, und liefern Sie für fremde Typen NotImplemented: Dann fragt Python den anderen Operanden und fällt zuletzt auf die Identität zurück. Sets und Dicts finden ein Objekt zuerst über seinen Hash und vergleichen erst dann mit ==. Die einzige Regel für __hash__ lautet daher: Gleiche Objekte müssen denselben Hash haben. Üblich ist, ein Tupel genau der Felder zu hashen, die __eq__ vergleicht: hash((self.x, self.y)).

Warum __eq__ allein unhashbar macht

Der geerbte Hash beruht auf der Identität und bräche die Regel, sobald zwei verschiedene Objekte gleich sind. Eine Klasse mit __eq__, aber ohne __hash__, bekommt deshalb __hash__ = None, und ein Objekt davon in einem Set löst TypeError aus. @dataclass macht es genauso, außer bei frozen=True. Ein Hash darf sich auch nie ändern, solange das Objekt in einem Set oder Dict liegt: Ändern Sie ein gehashtes Feld, liegt das Objekt im falschen Fach. Hashen Sie nur unveränderliche Werte.

Quellen

Zuletzt geprüft am 29. September 2026