Background
I am writing math utility classes ListVector
and TupleVector
,
inheriting from list
and tuple
respectively:
class ListVector(list):
...
class TupleVector(tuple):
...
(Aside: I'm not necessarily claiming this is really a good idea; in fact,
I'm well aware that, arguably, I should not do this,
since my intended relationships are logically "has-a" rather than "is-a",
and inappropriately making it "is-a" is dangerous since, if I'm not careful,
undesired behavior will leak from the base classes into my
classes, e.g. the behavior of operators +, +=, *, *=, ==, and
other gotchas described here.);
one possible advantage of using "is-a" anyway is that I expect there will be
little-to-no overhead in terms of memory and for getting/setting the
i'th element, compared to using list
and tuple
directly.)
I want to support comparison of ListVector
against TupleVector
using == and !=;
e.g. I want this to succeed:
assert ListVector((1,2,3)) == TupleVector((1,2,3))
Note that that's different from the base class behavior:
assert list((1,2,3)) != tuple((1,2,3))
i.e.
assert [1,2,3] != (1,2,3)
Therefore I'll need to override the __eq__
and __ne__
methods
in both of my vector classes.
The problem
I implemented the overrides for __eq__
and __ne__
in my TupleVector
class,
but I initially forgot to implement them in my ListVector
class.
No problem so far: I'm doing TDD, so my unit test should catch that mistake and force me to fix it.
But the unit test assertion that's supposed to catch the mistake unexpectedly succeeds:
assert ListVector((1,2,3)) == TupleVector((1,2,3)) # unexpectedly succeeds!
Expected behavior: since I forgot to override __eq__
and __ne__
in ListVector,
I expect the == call to fall through to list.__eq__
, which should return False
,
and so the assertion should fail.
Actual behavior: it calls reflected TupleVector.__eq__
instead,
which returns True
, and so the assertion succeeds!
The question
So my question is: why is it calling reflected TupleVector.__eq__
instead of (non-reflected) list.__eq__
?
According to the rules described here
(which is taken from this faq),
I think it should call list.__eq__
.
Specifically, it looks to me like the 2nd clause applies:
If
type(a)
has overridden__eq__
(that is,type(a).__eq__
isn’tobject.__eq__
), then the result isa.__eq__(b)
." where mytype(a)
andtype(b)
areListVector
andTupleVector
respectively.
My reading of the documentation also seems to lead to the same conclusion as the faq (that is, the left operand's method, i.e. list.__eq__
, should be called):
If the operands are of different types, and the right operand’s type is a direct or indirect subclass of the left operand’s type, the reflected method of the right operand has priority, otherwise the left operand’s method has priority.
Here is the code:
#!/usr/bin/python3
class ListVector(list):
# OOPS! Forgot to implement __eq__ and __ne__ for ListVector
# ...
pass
class TupleVector(tuple):
def __eq__(self, other):
print("TupleVector.__eq__ called")
# strict=True so comparing Vectors of unequal length will throw
return all(x==y for x,y in zip(self,other, strict=True))
def __ne__(self, other):
return not self.__eq__(other)
# ...
# Unit test
assert repr(ListVector((1,2,3))) == "[1, 2, 3]" # succeeds as expected
assert repr(TupleVector((1,2,3))) == "(1, 2, 3)" # succeeds as expected
assert TupleVector((1,2,3)) == ListVector((1,2,3)) # emits "TupleVector.__eq__ called" and succeeds, as expected
assert ListVector((1,2,3)) == TupleVector((1,2,3)) # WTF: unexpectedly emits "TupleVector.__eq__ called" and succeeds!
# Confirm that the condition "type(a).__eq__ isn’t object.__eq__", mentioned
# in the decision procedure in the FAQ, holds:
assert ListVector.__eq__ is list.__eq__ # because I forgot to override that
assert ListVector.__eq__ is not object.__eq__ # because list.__eq__ is not object.__eq__
assert TupleVector.__eq__ is not tuple.__eq__ # because I remembered that override
assert TupleVector.__eq__ is not object.__eq__ # definitely not
The (surprising) output is:
TupleVector.__eq__ called
TupleVector.__eq__ called
I expected that, instead, "TupleVector.__eq__ called
" should be emitted
only once instead of twice,
and the "assert ListVector((1,2,3)) == TupleVector((1,2,3))
" should fail.
__eq__
method can returnNotImplemented
, allowing Python to fall through to the next handler. You can observe this behavior byprint([].__eq__(()))
.