[Python] Why does a==b call b.__eq__ when derived from list and tuple with missing override?

Discussão em 'Python' iniciado por Stack, Abril 25, 2025.

  1. Stack

    Stack Membro Participativo

    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’t object.__eq__), then the result is a.__eq__(b)." where my type(a) and type(b) are ListVector and TupleVector 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.

    Continue reading...

Compartilhe esta Página