Korred Korred - 2 months ago 22
Python Question

Difference between a+b and a.__add__(b)

I am currently trying to understand where the difference between using

a+b
and
a.__add__(b)
is when it comes to custom classes. There are numerous websites that say that using the '+'-operator results in using the special method
__add__
- which is fine so far.

But when i run the following example I get two different results.

class C:
def __add__(self, other):
print("C.__add__", self, other)
return "result"
def __radd__(self, other):
print("C.__radd__", self, other)
return "reversed result"

c = C()
print(1+c)
print()
print(1 .__add__(c))
print(int.__add__(1,c))


Result:

C.__radd__ <C object at 0x7f60b92e9550> 1
reversed result

NotImplemented
NotImplemented


Now from what I understood, when executing
1+c
Python checks/executes the int
__add__
method - finds that there is no implementation for adding int and C objects - returns NotImplemented - which lets Python know to check object C for
__radd__
and executes the code within.

Why does
1+c
result in executing the
__radd__
code but the other two version are just returning
NotImplemented
without checking
__radd__
??

Answer

Based on the docs on "special" methods:

  • Regarding __add__():

    __add__() is called to implement the binary arithmetic "+" operation. For instance, to evaluate the expression x + y, where x is an instance of a class that has an __add__() method, x.__add__(y) is called.

    If one of those methods does not support the operation with the supplied arguments, it should return NotImplemented.

  • Regarding __radd__():

    These functions are only called if the left operand does not support the corresponding operation and the operands are of different types. For instance, to evaluate the expression x + y, where y is an instance of a class that has an __radd__() method, y.__radd__(x) is called if x.__add__(y) returns NotImplemented.

    If the right operand’s type is a subclass of the left operand’s type and that subclass provides the reflected method for the operation, this method will be called before the left operand’s non-reflected method. This behavior allows subclasses to override their ancestors’ operations.

Explanation with the examples based on the behaviour:

Case 1:

>>> print 1+c
('C.__radd__', <__main__.C instance at 0x7ff5631397a0>, 1)
reversed result

These functions radd are only called if the left operand does not support the corresponding operation and the operands are of different types. In this case, 1 does not support addition of the class hence, it falls back to the __radd__() function of the C class. In case __radd__ was not implement in C() class, it would have fallen back to __add__()

Case2:

>>> 1 .__add__(c)
NotImplemented
>>> c .__add__(1)
('C.__add__', <__main__.C instance at 0x7ff563139830>, 1)
'result'

1 .__add__(c) gives NotImplemented as 1 is of int type and add of int class do not supports add with C class object. But c .__add(1) run because C() class supports that.

Case 3:

>>> int.__add__(1, c)
NotImplemented
>>> C.__add__(c, 1)
('C.__add__', <__main__.C instance at 0x7ff5610add40>, 1)
'result'

Similar to case 2. But here, the call is made via class with first argument as object of that class. Behaviour would be same.

Case 4:

>>> int.__add__(c, 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor '__add__' requires a 'int' object but received a 'instance'
>>> C.__add__(1, c)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unbound method __add__() must be called with C instance as first argument (got int instance instead)

Vice-versa of case 3. As is cleared from the stack-trace, __add__ expected the object of the calling class as the first argument, failing which resulted in exception.