Search code examples
pythonpython-3.xoperatorscomparatorpython-3.9

Why are dunder methods inconsistant?


I am very confused. Say I have a class (which I do) where every operator and comparator (+, -, <, >=, ==, etc.) just returns itself. In case you don't understand, here's the code:

class _:
    def __init__(self, *args, **kwargs):
        pass
    def __add__(self, other):
        return self
    def __sub__(self, other):
        return self
    def __mul__(self, other):
        return self
    def __truediv__(self, other):
        return self
    def __floordiv__(self, other):
        return self
    def __call__(self, *args, **kwargs):
        return self
    def __eq__(self, other):
        return self
    def __lt__(self, other):
        return self
    def __gt__(self, other):
        return self
    def __ge__(self, other):
        return self
    def __le__(self, other):
        return self

I've noticed an inconsistency. The following work:

_()+_
_()-_
_()*_
_()/_
_()//_
_()>_
_()<_
_()==_
_()>=_
_()<=_
_<_()
_>_()
_==_()
_<=_()
_>=_()

But the following don't:

_+_()
_-_()
_*_()
_/_()
_//_()

They give the following error:

Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: unsupported operand type(s) for *: 'type' and '_'

In summary, comparators work with type vs instance both ways, but operators only work when the instance is on the left of the operator. Why is this?


Solution

  • The fact that you are using the type in these comparisons is irrelevant and confusing. You'll see the same behavior with any arbitrary object that doesn't implement the operators. So just create another class, class Foo: pass, and you'll see the same behavior if you used a Foo() instance. Or just an object() instance.

    Anyway, the arithmetic dunder methods all have a swapped argument version, e.g. for __add__ it's __radd__ (I think of it as "right add"). If you have x + y, and x.__add__ is not implemented, it tries to use y.__radd__.

    Now, for the comparison operators, there are no __req__ and __rgt__ operators. Instead, the other operators themselves do this. From the docs:

    There are no swapped-argument versions of these methods (to be used when the left argument does not support the operation but the right argument does); rather, __lt__() and __gt__() are each other’s reflection, __le__() and __ge__() are each other’s reflection, and __eq__() and __ne__() are their own reflection.

    So, in the cases where you have the type on the left, e.g.

    _<_()
    

    Then type.__lt__ doesn't exist, so it tries _.__gt__, which does exist.

    To demonstrate:

    >>> class Foo:
    ...     def __lt__(self, other):
    ...         print("in Foo.__lt__")
    ...         return self
    ...     def __gt__(self, other):
    ...         print("in Foo.__gt__")
    ...         return self
    ...
    >>> Foo() < Foo
    in Foo.__lt__
    <__main__.Foo object at 0x7fb056f696d0>
    >>> Foo < Foo()
    in Foo.__gt__
    <__main__.Foo object at 0x7fb0580013d0>
    

    Also, again, the fact that you are using the type of the instance is irrelevant. You'd get the same pattern with any other object that doesn't implement these operators:

    >>> Foo() < object()
    in Foo.__lt__
    <__main__.Foo object at 0x7fb056f696d0>
    >>> object() < Foo()
    in Foo.__gt__
    <__main__.Foo object at 0x7fb0580013d0>