我今天发现了一件奇怪的事情,并想知道是否有人可以了解这里的差异是什么?
import numpy as np
A = np.arange(12).reshape(4,3)
for a in A:
a = a + 1
B = np.arange(12).reshape(4,3)
for b in B:
b += 1
运行每个for
循环后,A
未更改,但B
已添加一个元素。我实际上使用B
版本写入for
循环内的初始化NumPy数组。
答案 0 :(得分:109)
不同之处在于,一个修改了数据结构本身(就地操作)b += 1
而另一个只是重新分配变量a = a + 1
。
为了完整性:
x += y
并不总是执行就地操作,至少有三个例外:
如果x
未实施 __iadd__
方法,则x += y
语句只是x = x + y
的简写。如果x
类似于int
。
如果__iadd__
返回NotImplemented
,则Python会回归x = x + y
。
理论上可以实现__iadd__
方法不起作用。但是,做到这一点真的很奇怪。
碰巧你的b
是numpy.ndarray
s,它实现__iadd__
并返回自身,所以你的第二个循环就地修改原始数组。
您可以在Python documentation of "Emulating Numeric Types"。
中详细了解相关信息调用这些[
__i*__
]方法来实现增强的算术分配(+=
,-=
,*=
,@=
,/=
,//=
,%=
,**=
,<<=
,>>=
,&=
,^=
,|=
)。这些方法应该尝试就地执行操作(修改self)并返回结果(可能是,但不一定是self)。如果未定义特定方法,则扩充分配将回退到常规方法。例如,如果x是具有__iadd__()
方法的类的实例,则x += y
等同于x = x.__iadd__(y)
。否则,与x.__add__(y)
的评估一样,会考虑y.__radd__(x)
和x + y
。在某些情况下,增强赋值可能会导致意外错误(请参阅Why doesa_tuple[i] += ["item"]
raise an exception when the addition works?),但此行为实际上是数据模型的一部分。
答案 1 :(得分:28)
在第一个示例中,您将重新分配变量a
,而在第二个示例中,您正在使用+=
运算符就地修改数据。
请参阅有关7.2.1. Augmented assignment statements 的部分:
像
x += 1
这样的扩充分配表达式可以重写为x = x + 1
,以实现类似但不完全相同的效果。在增强版本中,x仅评估一次。 此外,如果可能,实际操作是就地执行,这意味着不是创建新对象并将其分配给目标,而是修改旧对象。
+=
运营商呼叫__iadd__
。此函数进行就地更改,并且只有在执行后,结果才会返回到您正在“应用”+=
的对象。
__add__
获取参数并返回它们的总和(不修改它们)。
答案 2 :(得分:13)
正如已经指出的,b += 1
就地更新b
,而a = a + 1
计算a + 1
,然后将名称a
分配给结果(现在a
不再引用A
行。
要正确理解+=
运算符,我们还需要了解 mutable 与 immutable 对象的概念。考虑一下我们遗漏.reshape
:
C = np.arange(12)
for c in C:
c += 1
print(C) # [ 0 1 2 3 4 5 6 7 8 9 10 11]
我们发现C
未已更新,这意味着c += 1
和c = c + 1
是等效的。这是因为现在C
是一维数组(C.ndim == 1
),因此在迭代C
时,每个整数元素都被拉出并分配给c
。
现在在Python中,整数是不可变的,这意味着不允许就地更新,有效地将c += 1
转换为c = c + 1
,其中c
现在指的是 new < / em>整数,不以任何方式耦合到C
。循环重新整形的数组时,整行(np.ndarray
)一次分配给b
(和a
),这些 mutable 对象,这意味着你可以随意坚持新的整数,当你a += 1
时就会发生这种情况。
应该提到的是,尽管+
和+=
意味着如上所述(通常也是如此),但任何类型都可以通过定义{{{}来实现它们。分别为1}}和__iadd__
方法。
答案 3 :(得分:4)
简短形式(a += 1
)可以选择就地修改a
,而不是创建表示总和的新对象并将其重新绑定到相同的名称(a = a + 1
因此,简短形式(a += 1
)非常有效,因为它不一定需要复制a
而不是a = a + 1
。
即使他们输出相同的结果,也要注意它们是不同的,因为它们是不同的运算符:+
和+=
答案 4 :(得分:3)
首先关闭:循环中的变量a和b引用a = a + 1
个对象。
在第一个循环中,__add__(self, other)
的计算方法如下:调用numpy.ndarray
的{{1}}函数。这会创建一个新对象,因此不会修改A.然后,变量a
被设置为引用结果。
在第二个循环中,不会创建新对象。语句b += 1
调用__iadd__(self, other)
numpy.ndarray
函数,该函数修改了b所指的ndarray
对象。因此,B
已被修改。
答案 5 :(得分:2)
这里的一个关键问题是这个循环遍历B
的行(第一维):
In [258]: B
Out[258]:
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
In [259]: for b in B:
...: print(b,'=>',end='')
...: b += 1
...: print(b)
...:
[0 1 2] =>[1 2 3]
[3 4 5] =>[4 5 6]
[6 7 8] =>[7 8 9]
[ 9 10 11] =>[10 11 12]
因此+=
作用于一个可变对象,一个数组。
其他答案中暗示了这一点,但如果您专注于a = a+1
重新分配,则很容易错过。
我还可以使用b
索引对[:]
进行就地更改,甚至更热情,b[1:]=0
:
In [260]: for b in B:
...: print(b,'=>',end='')
...: b[:] = b * 2
[1 2 3] =>[2 4 6]
[4 5 6] =>[ 8 10 12]
[7 8 9] =>[14 16 18]
[10 11 12] =>[20 22 24]
当然对于像B
这样的二维数组,我们通常不需要对行进行迭代。许多在B
上运行的操作也适用于整个事情。 B += 1
,B[1:] = 0
等