6

In Python, is it possible for your class's __rmul__ method to override another class's __mul__ method, without making changes to the other class?

This question arises since I'm writing a class for a certain type of linear operator, and I want it to be able to multiply numpy arrays using the multiplication syntax. Here is a minimal example illustrating the issue:

import numpy as np    

class AbstractMatrix(object):
    def __init__(self):
        self.data = np.array([[1, 2],[3, 4]])

    def __mul__(self, other):
        return np.dot(self.data, other)

    def __rmul__(self, other):
        return np.dot(other, self.data)

Left multiplication works fine:

In[11]: A = AbstractMatrix()
In[12]: B = np.array([[4, 5],[6, 7]])
In[13]: A*B
Out[13]: 
array([[16, 19],
       [36, 43]])

But right multiplication defaults to np.ndarray's version, which splits the array up and performs multiplication element-by-element (this not what is desired):

In[14]: B*A
Out[14]: 
array([[array([[ 4,  8],
       [12, 16]]),
        array([[ 5, 10],
       [15, 20]])],
       [array([[ 6, 12],
       [18, 24]]),
        array([[ 7, 14],
       [21, 28]])]], dtype=object)

In this situation, how can I make it call my own class's __rmul__ on the original (unsplit) array?

Answers addressing the specific case of numpy arrays are welcome but I am also interested in the general idea of overriding methods of another third party class that cannot be modified.

MSeifert
  • 145,886
  • 38
  • 333
  • 352
Nick Alger
  • 984
  • 7
  • 26
  • 1
    Why don't you use the `@` operator? – Francisco Oct 26 '16 at 02:33
  • http://stackoverflow.com/a/5182501/2823755 – wwii Oct 26 '16 at 02:51
  • 1
    I believe that the only circumstance when __rXXX__ methods get checked before the usual __XXX__ methods are when the right-side object is a subclass of the left-side object. I don't know enough about numpy internals to know whether they even can be subclassed. – jasonharper Oct 26 '16 at 02:51
  • [From the docs](https://docs.python.org/3/reference/datamodel.html#object.__rmul__) - ```These functions are only called if the left operand does not support the corresponding operation and the operands are of different types``` – wwii Oct 26 '16 at 02:53
  • @FranciscoCouzo: OP doesn't say what version is being targeted. `@` operator is _very_ new, [only added in 3.5](https://docs.python.org/3/whatsnew/3.5.html#pep-465-a-dedicated-infix-operator-for-matrix-multiplication). – ShadowRanger Oct 26 '16 at 02:53
  • @jasonnharper I thought it was when the left operand was a base type or a subclass of one, you might be right, or maybe we both are – Jared Goguen Oct 26 '16 at 04:20
  • 1
    @FranciscoCouzo As someone doing numerical analysis, the @ operator looks amazing! What a great decision by the python community. I'm using Python 2.7.6, but this alone might get me to go to 3.x. – Nick Alger Oct 26 '16 at 05:24

1 Answers1

5

The easiest way to make NumPy respect your __rmul__ method is to set an __array_priority__:

class AbstractMatrix(object):
    def __init__(self):
        self.data = np.array([[1, 2],[3, 4]])

    def __mul__(self, other):
        return np.dot(self.data, other)

    def __rmul__(self, other):
        return np.dot(other, self.data)

    __array_priority__ = 10000

A = AbstractMatrix()
B = np.array([[4, 5],[6, 7]])

This works like expected.

>>> B*A
array([[19, 28],
       [27, 40]])

The problem is that NumPy doesn't respect Pythons "Numeric" Data model. If a numpy array is the first argument and numpy.ndarray.__mul__ isn't possible then it tries something like:

result = np.empty(B.shape, dtype=object)
for idx, item in np.ndenumerate(B):
    result[idx] = A.__rmul__(item)

However if the second argument has an __array_priority__ and it's higher than the one of the first argument only then it really uses:

A.__rmul__(B)

However since Python 3.5 (PEP-465) there is the @ (__matmul__) operator that can utilize matrix multiplication:

>>> A = np.array([[1, 2],[3, 4]])
>>> B = np.array([[4, 5],[6, 7]])
>>> B @ A
array([[19, 28],
       [27, 40]])
MSeifert
  • 145,886
  • 38
  • 333
  • 352