Перегрузка оператора в Python: обработка различных типов и порядок параметров

У меня есть простой класс, который помогает с математическими операциями над векторами (т.е. списками чисел). Мой Vector может быть умножен на другие экземпляры Vector или скаляр (float или int).

В других, более строго типизированных языках я бы создал метод для умножения двух Vector и отдельного метода для умножения a Vector на и int/float. Я все еще довольно новичок в Python и не знаю, как это реализовать. Единственный способ, которым я могу это сделать, - переопределить __mul__() и проверить входящий параметр:

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

Даже если я это сделаю, мне пришлось бы умножить Vector на такой скаляр:

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

result = v * 7

Что делать, если я хотел бы изменить порядок операндов при умножении?

result = 7 * v

Каков правильный способ сделать это в Python?

Ответы

Ответ 1

Вам также необходимо реализовать __rmul__. Когда начальный вызов int.__mul__(7, v) завершается с ошибкой, Python попытается выполнить следующую попытку type(v).__rmul__(v, 7).

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

Как указывает Rawing, вы можете просто написать __rmul__ = __mul__ для этого определения. __rmul__ существует, чтобы допускать некоммутативное умножение, когда просто отложить до __mul__ с обратными операндами недостаточно.

Например, если вы пишете класс Matrix и хотите поддерживать умножение вложенным списком, например,

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

Здесь класс list не знал бы, как многократный список экземпляром Matrix, поэтому, когда list.__mul__(n, m) не работает, Python попытается попробовать Matrix.__rmul__(m, n). Однако n * m и m * n представляют собой два разных результата в целом, поэтому Matrix.__rmul__(m, n) != Matrix.__mul__(m, n); __rmul__ должен сделать небольшую дополнительную работу для получения правильного ответа.

Ответ 2

специальные методы для обратных операций:

  • __rmul__ для обратного __mul__
  • и __radd__ для __add__,
  • ...

Они вызывается, когда оператор левой стороны возвращает NotImplemented для нормальной работы (поэтому сначала будет выполняться операция 2 + vector_instance: (2).__add__(vector_instance), но если это возвращает NotImplemented, то вызывается vector_instance.__radd__(2)).

Однако я не использовал бы теги isinstance в арифметических специальных методах, что приведет к много повторения кода.

Фактически вы можете создать специальный случай в __init__ и реализовать преобразование из скаляров в Vector там:

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)

Это должно работать как ожидалось:

>>> 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)

Обратите внимание, что это был быстрый и грязный проект (который может содержать несколько ошибок). Я просто хотел представить "общую идею" о том, как ее можно было бы решить без специального обтекания типа в каждой арифметической операции.