What you ask can be done, and easily. For instance:
class dundersuper(super):
def __add__(self, other):
# this works, because the __getattribute__ method of super is over-ridden to search
# through the given object's mro instead of super's.
return self.__add__(other)
super = dundersuper
class MyInt(int):
def __add__(self, other):
return MyInt(super() + other)
i = MyInt(0)
assert type(i + 1) is MyInt
assert i + 1 == MyInt(1)
So the reason that super works with magic methods isn't because it's not possible. The reason must lie elsewhere. One reason is that doing so would violate the contract of equals (==
). That is equals is, amongst other criteria, symmetric. This means that if a == b
is true then b == a
must also be true. That lands us in a tricky situation, where super(self, CurrentClass) == self
, but self != super(self, CurrentClass)
eg.
class dundersuper(super):
def __eq__(self, other):
return self.__eq__(other)
super = dundersuper
class A:
def self_is_other(self, other):
return super() == other # a.k.a. object.__eq__(self, other) or self is other
def __eq__(self, other):
"""equal if both of type A"""
return A is type(self) and A is type(other)
class B:
def self_is_other(self, other):
return other == super() # a.k.a object.__eq__(other, super()), ie. False
def __eq__(self, other):
return B is type(self) and B is type(other)
assert A() == A()
a = A()
assert a.self_is_other(a)
assert B() == B()
b = B()
assert b.self_is_other(b) # assertion fails
Another reason is that once super is done searching it's given object's mro, it then has to give itself a chance to provide the requested attribute - super objects are still an objects in their own right -- we should be able to test for equality with other objects, ask for string representations, and introspect the object and class super is working with. This creates a problem if the dunder method is available on the super object, but not on object that the mutable object represents. For instance:
class dundersuper(super):
def __add__(self, other):
return self.__add__(other)
def __iadd__(self, other):
return self.__iadd__(other)
super = dundersuper
class MyDoubleList(list):
"""Working, but clunky example."""
def __add__(self, other):
return MyDoubleList(super() + 2 * other)
def __iadd__(self, other):
s = super()
s += 2 * other # can't assign to the result of a function, so we must assign
# the super object to a local variable first
return s
class MyDoubleTuple(tuple):
"""Broken example -- iadd creates infinite recursion"""
def __add__(self, other):
return MyDoubleTuple(super() + 2 * other)
def __iadd__(self, other):
s = super()
s += 2 * other
return s
With the list example the function __iadd__
could have been more simply written as
def __iadd__(self, other):
return super().__iadd__(other)
With the tuple example we fall into infinite recursion, this is because tuple.__iadd__
does not exist. Therefore when looking up the attribute __iadd__
on a super object the actual super object is checked for an __iadd__
attribute (which does exist). We get that method and call it, which starts the whole process again. If we'd not written an __iadd__
method on super and used super().__iadd__(other)
then this would never have happened. Rather we'd get an error message about a super object not having the attribute __iadd__
. Slightly cryptic, but less so than an infinite stack trace.
So the reason super doesn't work with magic methods is that it creates more problems than it solves.