MikeL
MikeL

Reputation: 2469

Overloading operators for operands of different types in Python

Consider the following example of a 'wrapper' class to represent vectors:

class Vector:

    def __init__(self, value):
        self._vals = value.copy()


    def __add__(self, other):

        if isinstance(other, list):
            result = [x+y for (x, y) in zip(self._vals, other)]
        elif isinstance(other, Vector):
            result = [x+y for (x, y) in zip(self._vals, other._vals)]
        else:
            # assume other is scalar
            result = [x+other for x in self._vals]

        return Vector(result)

    def __str__(self):
        return str(self._vals)

The __add__ method takes care of adding two vectors as well as adding a vector with a scalar. However, the second case is not complete as the following examples show:

>>> a = Vector([1.2, 3, 4])
>>> print(a)
[1.2, 3, 4]
>>> print(a+a)
[2.4, 6, 8]
>>> print(a+5)
[6.2, 8, 9]
>>> print(5+a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'Vector'

To my understanding the reason is that the overloaded operator only tells Python what to do when it sees a + x where a is an instance of Vector, but there is no indication of what to do for x + a (with a an instance of Vector and x a scalar).

How one should overload the operators in such circumstances to cover all cases (i.e., to support the case that self is not an instance of Vector but other is)?

Upvotes: 3

Views: 709

Answers (3)

gstukelj
gstukelj

Reputation: 2551

You already figured out you need to implement __radd__. This is an answer as to why this is so, and why you need to do this in addition to implementing __add__, as a Both quotes are taken from Python Docs (Data Model - 3.3.8 Emulating numeric types), starting with the obvious:

These methods are called to implement the binary arithmetic operations (+, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |). 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.

So order determines which object's implementation of __add__ is called. When the method doesn't support the operation with the passed argument NotImplemented should be returned. That's where the so-called "reflected methods" come into play:

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 __rsub__() method, y.__rsub__(x) is called if x.__sub__(y) returns NotImplemented [sic].

Now, why wouldn't __radd__(self, other) just fall back to __add__(self, other)? While ring addition is always commutative (see this and this math.stackexchange answers), you could have algebraic structures that are do not satisfy this assumption (e.g., near-rings). But my guess as a non-mathematician would be that it's just desirable to have a consistent data model across different numerical methods. While addition might be commonly commutative, multiplication is less so. (Think matrices and vectors! Although, admittedly this is not the best example, given __matmul__). I also prefer to see there being no exceptions, especially if I had to read about rings, etc. in a language documentation.

Upvotes: 0

MikeL
MikeL

Reputation: 2469

Ok. I guess I found the answer: one has to overload __radd__ operator as well:

class Vector:

    def __init__(self, value):
        self._vals = value.copy()


    def __add__(self, other):

        if isinstance(other, list):
            result = [x+y for (x, y) in zip(self._vals, other)]
        elif isinstance(other, Vector):
            result = [x+y for (x, y) in zip(self._vals, other._vals)]
        else:
            # assume other is scalar
            result = [x+other for x in self._vals]

        return Vector(result)

    def __radd__(self, other):
        return self + other


    def __str__(self):
        return str(self._vals)

Although to me this looks a bit redundant. (Why Python does not use the commutativity of addition by default, assuming __radd__(self, other) always returns self + other? Of course for special cases the user can override __radd__.)

Upvotes: 1

Ollie
Ollie

Reputation: 1712

You could define a Scalar class that has int as its base class.

Then override __add__ to do what you want.

class Scalar(int):
    def __add__(self):
        # do stuff

Upvotes: 0

Related Questions