0%

Objekte darstellen und vergleichen · Übung

Wertgleichheit definieren

Question ist ein Wertobjekt. Zwei Fragen mit demselben Inhalt sollten daher gleich sein. Um Python das mitzuteilen, brauchst du eine Methode.

== definieren

Try it

== vergleicht nun Inhalte. is vergleicht weiterhin Objekte und wird das immer tun: Identität darf eine Klasse nicht neu definieren.

Alles, was auf == aufbaut, verbessert sich gleichzeitig:

Try it

Der Mitgliedschaftstest aus der vorherigen Lektion funktioniert nun, ebenso .index(), .remove() und der Vergleich zweier Listen.

Mit etwas vergleichen, das keine Frage ist

Dieses __eq__ hat einen Fehler. Es ist der Fehler, den fast jeder zuerst macht:

first == "What does len('cat') return?"

AttributeError, weil ein String kein .prompt hat. Zwei Dinge zu vergleichen sollte niemals eine Ausnahme auslösen. == wird von in, Listenvergleichen, .index(), .remove() und Testframeworks verwendet, oft bei Sammlungen, deren Inhalte du nicht gewählt hast. Sortieren ist etwas anderes: Es verwendet Ordnungsvergleiche oder eine Schlüsselfunktion.

Die Lösung ist, zuerst den Typ zu prüfen:

Try it

isinstance(other, Question) fragt, ob other ein Question ist. Falls nicht, gibt die Methode NotImplemented zurück.

Das sieht zunächst nach einem Fehler aus und verdient eine Erklärung. NotImplemented ist ein spezieller Wert mit der Bedeutung „Ich weiß nicht, wie ich mich damit vergleichen soll“. Python versteht ihn als Antwort, nicht als Endergebnis: Es fragt dann das andere Objekt, ob es den Vergleich kennt. Weiß keines Bescheid, fällt Python auf den Identitätsvergleich zurück, der hier False ergibt. Direkt False zurückzugeben würde hier ebenfalls funktionieren, würde aber eine andere Klasse unbemerkt daran hindern, sich selbst als gleich zu einer Frage zu erklären. NotImplemented zurückzugeben ist die bessere Gewohnheit.

Warum prüft __eq__ vor dem Attributvergleich isinstance(other, Question)?

Entscheiden, was zählt

__eq__ ist eine Entwurfsentscheidung, keine Formalität. Du legst fest, welche Teile eines Objekts seinen Wert ausmachen.

Betrachte ein Response, das zusätzlich den Zeitpunkt der Antwort erfasst:

class Response:
    def __init__(self, question, given, answered_at):
        ...

Sollen zwei Antworten auf dieselbe Frage mit demselben Text, aber einer Sekunde Abstand gleich sein? Darauf gibt es keine allgemeine Antwort. Ist Response ein Wert, der beschreibt, was gesagt wurde, ist die Zeit nebensächlich und sollte ausgelassen werden. Ist es eine Aufzeichnung eines Ereignisses, gehört der Zeitpunkt zum Geschehen, und zwei Ereignisse sind niemals dasselbe Ereignis.

Zwei Regeln gelten unabhängig davon:

Bevorzuge stabilen Wertzustand. Veränderbare Objekte wie Listen können jetzt gleich und später ungleich sein. Das ist zulässig. Für eine Klasse, die sich wie ein Wert verhalten soll, macht schreibgeschützter öffentlicher Zustand Gleichheit jedoch leichter verständlich. Die nächste Lektion erklärt die strengere Regel, die gelten muss, bevor ein Objekt gehasht und als Dictionary-Schlüssel verwendet werden darf.

Mache die repr-Darstellung zum hilfreichen Begleiter der Gleichheit. Die verglichenen Felder anzuzeigen erklärt, warum sich zwei Werte unterscheiden. Eine Zusammenfassung darf Einzelheiten weglassen. Übereinstimmende repr-Darstellungen allein beweisen deshalb keine Gleichheit.

Die Gewohnheit

Von Gleichheit erwarten wir: Ein Objekt ist sich selbst gleich; a == b stimmt mit b == a überein; und wenn a == b und b == c gelten, gilt auch a == c. Der Vergleich gewöhnlicher Strings und Zahlen in diesem Beispiel erhält diese Erwartungen. Auch einen String auf beiden Seiten vor dem Vergleich zu normalisieren kann sie erhalten. Eine Regel wie „Beträge mit höchstens einem Punkt Abstand sind gleich“ tut das nicht: 1 passt zu 2 und 2 zu 3, aber 1 nicht zu 3. Halte solche Näheprüfungen in einer eigenen Methode.

Die Übung ergänzt Gleichheit für zwei schreibgeschützte Werte: Question und Points, einen positiven Betrag. Das über seine Identität bestimmte QuizAttempt erhält keine eigene Gleichheit.

Aufgabe

Gib den beiden passenden Klassen Wertgleichheit und keiner anderen.

Question ist gleich, wenn Fragetext, erwartete Antwort und Punkte alle übereinstimmen. Points ist gleich, wenn sein Betrag übereinstimmt.

Beide müssen bei einem Vergleich mit etwas außerhalb ihrer eigenen Klasse NotImplemented zurückgeben, damit question == "text" den Wert False meldet, statt eine Ausnahme auszulösen.

Lass QuizAttempt unverändert. Zwei Versuche sind nur dann derselbe Versuch, wenn sie dasselbe Objekt sind. Das tut Python bereits. Ein zusätzliches __eq__ wäre hier daher falsch.

Führe das Programm aus. Die Funktion find_duplicate am Ende sollte die wiederholte Frage melden. Derzeit kann sie das noch gar nicht.