Representing and Comparing Objects · practice
Define Value Equality
Question is a
Defining ==
== now compares contents. is still compares objects, and always will: identity is not something a class gets to redefine.
Everything built on == improves at the same time:
The membership test from the previous lesson now works, and so do .index(), .remove(), and comparing two
Comparing with something that is not a question
That __eq__ has a bug, and it is the one everybody writes first:
first == "What does len('cat') return?"
AttributeError, because a .prompt. Comparing two things should never raise. == is used by in, list comparisons, .index(), .remove(), and test frameworks, often on collections whose contents you did not choose. Sorting is different: it uses ordering comparisons or a key
The fix is to check the type first:
isinstance(other, Question) asks whether other is a Question. If it is not, the method returns NotImplemented.
That looks like it should be a mistake, and it is worth one paragraph. NotImplemented is a special value meaning “I do not know how to compare myself with that.” Python takes it as an answer, not as a result: it then asks the other object whether it knows, and if neither does, it falls back to comparing identity, which gives False. Returning False directly would also work here, and it would quietly stop some other class from ever declaring itself equal to a question. Returning NotImplemented is the habit worth having.
Why does __eq__ check isinstance(other, Question) before comparing attributes?
Choosing what counts
__eq__ is a design decision, not a formality. You are declaring which parts of an object make it what it is.
Consider a Response that also records when it was answered:
class Response:
def __init__(self, question, given, answered_at):
...
Should two responses to the same question, with the same text, one second apart, be equal? There is no universal answer. If Response is a value describing what was said, the time is incidental and should be left out. If it is a record of an event, the time is part of what happened, and two events are never the same event.
Two rules that hold regardless:
Prefer stable value state.
Make the repr useful for understanding equality. Showing the compared fields helps explain why two values differ. A summary can omit detail, so matching reprs alone do not prove that objects are equal.
The habit
Equality is expected to behave like equality: an object equals itself, a == b agrees with b == a, and if a == b and b == c then a == c. Comparing the ordinary strings and numbers in this example preserves those expectations. Normalizing a string on both sides before comparing can preserve them too. A rule such as “amounts within one point are equal” does not: 1 matches 2 and 2 matches 3, but 1 does not match 3. Keep that kind of closeness check in a separate method.
The exercise adds equality to two read-only values: Question and Points, a positive amount. It leaves equality off the identity-based QuizAttempt.
Task
Give
Question is equal when its prompt, answer, and points all match. Points is equal when its amount matches.
Both must return NotImplemented when compared with something that is not their own class, so that question == "text" reports False instead of raising.
Leave QuizAttempt alone. Two attempts are the same attempt only when they are the same __eq__ there would be actively wrong.
Run the program. The find_duplicate