Numpy evaluation order

96 views Asked by At

As I was looking at some issues related to how Numpy indexing works (sometimes a view, sometimes a copy), I encountered an example in the Numpy docs, which baffles me a bit. Specifically, it can be found here.

Very simplistically, I don't understand the reason why the following snippet

import numpy as np

x = np.arange(2)
x[[0, 0]] += 1
print(x)

produces the result that it does.

I understand that the reason you get what you get is that x[[0, 0]] += 1 seems to expand to something like x.setitem(x.getitem([0, 0]).iadd(1)), but I don't understand why that's the case (you can test that this is what happens if you subclass ndarray and add messages to the relevant methods).

Given the precedence rules, isn't the statement x[[0, 0]] += 1 first converted to x[[0, 0]] = x[[0, 0]] + 1, which means the subsequent expansion should look more likex.getitem([0, 0]).setitem(x.getitem([0, 0]).iadd(1))? I appreciate that this would be awkward, but I don't really understand the formal logic that produces the expansion that allows the modification of the original array.

Is there an implicit binding that takes precedence over subscription?

I tried

import numpy as np

class A(np.ndarray):
    def __getitem__(self, *args, **kwargs):
        print("getitem")
        r = np.ndarray.__getitem__(self, *args, **kwargs)
        return r
    def __setitem__(self, *args, **kwargs):
        print("setitem")
        r = np.ndarray.__setitem__(self, *args, **kwargs)
        return r 
    def __iadd__(self, *args, **kwargs):
        print("iadd")
        r = np.ndarray.__iadd__(self, *args, **kwargs)
        return r

nd = np.arange(2)
x = nd.view(A)
x[[0, 0]] += 1
print(x)
2

There are 2 answers

2
Martinghoul On

A bit more digging makes it even more puzzling, as far as I am concerned.

I used ast to look at what the interpreter does with the following block:

import numpy as np
x = np.arange(2)
x[[0, 0]] += 1

The full output is:

("Module(body=[Import(names=[alias(name='numpy', asname='np')]), "
 "Assign(targets=[Name(id='x', ctx=Store())], "
 "value=Call(func=Attribute(value=Name(id='np', ctx=Load()), attr='arange', "
 'ctx=Load()), args=[Constant(value=2)], keywords=[])), '
 "AugAssign(target=Subscript(value=Name(id='x', ctx=Load()), "
 'slice=List(elts=[Constant(value=0), Constant(value=0)], ctx=Load()), '
 'ctx=Store()), op=Add(), value=Constant(value=1))], type_ignores=[])')

The line that shows the expansion of x[[0,0]] += 1, formatted to the best of my ability, is:

AugAssign(
    target=Subscript(
            value=Name(id='x', ctx=Load()), 
            slice=List(elts=[Constant(value=0), Constant(value=0)], ctx=Load()),
            ctx=Store()
    ), 
    op=Add(), 
    value=Constant(value=1)
)

So, based on this, the target for the AugAssign should be the result of the Subscript operation, so how does it end up being x.setitem(x.getitem([0, 0]).iadd(1)) ? This still doesn't make sense...

0
Martinghoul On

I think I finally have a full understanding...

Numpy documentation which describes how advanced indexing creates copies, rather than views, refers to cases where an expression such as x[[1, 2]] is used in the context of a reference (i.e. it is on the right-hand side). As the same docs specifically mention here,

"...during the assignment of x[[1, 2]] no view or copy is created..."

While this is kinda helpful, the reasoning is not given. However, it can be found in the Python reference docs for basic assignment statements here. The specific logic that the documentation describes for subscriptions and slicings explains why there is no copy created when x[[1, 2]] is the assignment target (is on the left-hand side).