Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Operator overloading in Python: handling different types and order of parameters [duplicate]

Tags:

I have a simple class that helps with mathematical operations on vectors (i.e. lists of numbers). My Vector can be multiplied by other instances of Vector or a scalar (float or int).

In other, more strongly typed, languages I would create a method to multiply two vectors and a separate method to multiply a vector by and int/float. I'm still pretty new to Python and am not sure how I would implement this. The only way I can think of doing it is override __mul__() and test the incoming parameter:

class Vector(object):
  ...
 def __mul__(self, rhs):
  if isinstance(rhs, Vector):
     ...
  if isinstance(rhs, int) or isinstance(rhs, float):
    ...

Even if I do it that way I would be forced to multiply a Vector by a scalar like this:

v = Vector([1,2,3])

result = v * 7

What if I wanted to reverse the order of the operands in the multiplication?

result = 7 * v

What is the right way to do that in Python?

like image 965
RobertJoseph Avatar asked Jun 13 '17 12:06

RobertJoseph


People also ask

How does Python handle overloading?

There isn't any method overloading in Python. You can however use default arguments, as follows. When you pass it an argument, it will follow the logic of the first condition and execute the first print statement. When you pass it no arguments, it will go into the else condition and execute the second print statement.

How many types of operator overloading are there?

Operator function must be either non-static (member function) or friend function. Overloading unary operator. Overloading binary operator. Overloading binary operator using a friend function.


2 Answers

There are special methods for reversed operations:

  • __rmul__ for the reverse of __mul__
  • and __radd__ for __add__,
  • ...

These are called when the left hand side operator returns NotImplemented for the normal operation (so the operation 2 + vector_instance will first try: (2).__add__(vector_instance) but if this returns NotImplemented then vector_instance.__radd__(2) is called).

However I wouldn't use isinstance checks in the arithmetic special methods, that will lead to a lot of code repetition.

You could actually create a special case in __init__ and implement a conversion from scalars to a Vector there:

class Vector(object):
    def __init__(self, x, y=None, z=None):
        if y is None and z is None:
            if isinstance(x, Vector):
                self.x, self.y, self.z = x.x, x.y, x.z
            else:
                self.x, self.y, self.z = x, x, x
        elif y is None or z is None:
            raise ValueError('Either x, y and z must be given or only x')
        else:
            self.x, self.y, self.z = x, y, z

    def __mul__(self, other):
        other = Vector(other)
        return Vector(self.x*other.x, self.y*other.y, self.z*other.z)

    __rmul__ = __mul__   # commutative operation

    def __sub__(self, other):
        other = Vector(other)
        return Vector(self.x-other.x, self.y-other.y, self.z-other.z)

    def __rsub__(self, other):   # not commutative operation
        other = Vector(other)
        return other - self

    def __repr__(self):
        return 'Vector({self.x}, {self.y}, {self.z})'.format(self=self)

This should work as expected:

>>> 2 - Vector(1, 2, 3)
Vector(1, 0, -1)

>>> Vector(1, 2, 3) - 2
Vector(-1, 0, 1)

>>> Vector(1, 2, 3) * 2
Vector(2, 4, 6)

>>> 2 * Vector(1, 2, 3)
Vector(2, 4, 6)

Note that this was a quick and dirty draft (that could have several bugs). I just wanted to present the "general idea" how it could be solved without special casing the type in each arithmetic operation.

like image 36
MSeifert Avatar answered Sep 30 '22 19:09

MSeifert


You also need to implement __rmul__. When the initial call to int.__mul__(7, v) fails, Python will next try type(v).__rmul__(v, 7).

def __rmul__(self, lhs):
    return self * lhs  # Effectively, turn 7 * v into v * 7

As Rawing points out, you could simply write __rmul__ = __mul__ for this definition. __rmul__ exists to allow for non-commutative multiplication where simply deferring to __mul__ with the operands reversed isn't sufficient.

For instance, if you were writing a Matrix class and wanted to support multiplication by a nested list, e.g.,

m = Matrix(...)  # Some 2 x 2 matrix
n = [[1, 2], [3,4]]
p = n * m

Here, the list class wouldn't know how to multiple a list by a Matrix instance, so when list.__mul__(n, m) fails, Python would next try Matrix.__rmul__(m, n). However, n * m and m * n are two different results in general, so Matrix.__rmul__(m, n) != Matrix.__mul__(m, n); __rmul__ has to do a little extra work to generate the right answer.

like image 98
chepner Avatar answered Sep 30 '22 18:09

chepner