3

In the python documentation, it is said that __mul__ will be firstly called to implement the binary arithmetic operations *. __rmul__ will not be called unless __mul__ is not supported or the right operand is subclass of the left operand. However, for the following code:

import numpy as np

a = [1, 2, 3]
b = np.array(2)

print("a * b:", a * b)
print("a.__mul__(b):", a.__mul__(b))
print("b.__rmul__(a):", b.__rmul__(a))

My first thought was that the result of a * b should be [1, 2, 3, 1, 2, 3], just identical to a * 2. However, the actual output is:

a * b: [2 4 6]
a.__mul__(b): [1, 2, 3, 1, 2, 3]
b.__rmul__(a): [2 4 6]

It seems that a * b calls b.__rmul__(a). However, in this case, a.__mul__(b) is implemented and gives the expected result, and np.ndarray is apparently not the subclass of list.

So, my question is, what is really going on in this example, and how does python choose between the binary arithmetic operations and their reflected operands?

UPDATE: Thanks to hpaulj, something more interesting is found from the following code:

import numpy as np

class Foo1(list):
    pass

class Foo2(list):
    def __mul__(self, other):
        print('in Foo2.__mul__')
        return super().__mul__(other)

print('Foo1:')
print(Foo1([1, 2, 3]) * np.array(3))
print('Foo2:')
print(Foo2([1, 2, 3]) * np.array(3))

The output is:

Foo1:
[3 6 9]
Foo2:
in Foo2.__mul__
[1, 2, 3, 1, 2, 3, 1, 2, 3]

As both Foo1 and Foo2 inherit from list, the only difference is Foo2 overwrites __mul__, and explicitly calls super().__mul__. But the Foo1 and Foo2 give totally different result! So, as hpaulj said, there should be some sort of special relation between list and np.ndarray. However, it really seems weird as np.ndarray comes from a third-party library and there should be no way it can modify the behavior of builtin types.

z.ni
  • 31
  • 3
  • In my experience, a numpy array has 'control' of such an operation, converting the list to array. I recall digging into the hows and whys not long ago, but forget the details. – hpaulj Aug 26 '21 at 09:31
  • This is what I was remembering, but not sure if it explains your case, https://stackoverflow.com/q/68302275/901925 – hpaulj Aug 26 '21 at 09:52
  • `[1,2,3]*np.int32(3)` does replicate. `[1,2,3]*np.array([1,2])` gives a broadcasting error, (3,) shape with (2,). Other SO that explore [numpy] and `__mul__` or `__rmul__` do so in the context of a custom dtype. It may be hard, working just from docs and example, to determine exactly why the array controls this operation. – hpaulj Aug 26 '21 at 15:39
  • When I make a class that inherits from list, the `Foo([1,2,3])*np.array(3)` behaves a you expect. So there must be some sort special relation between `list` and `ndarray` that doesn't carry over to user defined classes. I expect it's implemented within `numpy`, but I don't know how it relates to the interpreter's use of `__rmul__` and `__mul__`. – hpaulj Aug 26 '21 at 20:05
  • @hpaulj The answer from [stackoverflow.com/q/68302275/901925](https://stackoverflow.com/q/68302275/901925) is really helpful. In that case, the right oprand's type is subclass of the left oprand's type. But in this case, `np.ndarray` is not `list`'s subclass. So, there might be some other reason's for this problem. – z.ni Aug 27 '21 at 02:31
  • I suspect that list objects do not *really* implement `__mul__` — notice how sequence repetition is special-cased in [PyNumber_Multiply](https://github.com/python/cpython/blob/245f1f260577a005fd631144b4377febef0b47ed/Objects/abstract.c#L1107), and that the ["as-number methods"](https://github.com/python/cpython/blob/245f1f260577a005fd631144b4377febef0b47ed/Objects/listobject.c#L3052) of the list type is NULL. Obviously, this does not explain why lists apparently have a `__mul__` method... – Ture Pålsson Aug 27 '21 at 07:20

0 Answers0