I = i + 1和 i + = 1在 for 循环中的区别是什么?

我今天发现了一件奇怪的事情,想知道是否有人可以解释一下这里有什么不同?

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 数组。

259709 次浏览

区别在于,一个修改数据结构本身(就地操作) b += 1,而另一个只修改 重新分配变量 a = a + 1


只是为了完整起见:

x += y并不总是执行就地操作,(至少)有三个例外:

  • 如果 x 无法实现__iadd__方法,那么 x += y语句只是 x = x + y的简写。如果 x是类似于 int的东西,情况就是这样。

  • 如果 __iadd__返回 NotImplemented,Python 就返回到 x = x + y

  • 从理论上讲,__iadd__方法可以实现为不能正常工作。

碰巧您的 b是实现 __iadd__并返回自身的 numpy.ndarray,所以您的第二个循环就地修改原始数组。

你可以在 “模拟数值类型”的 Python 文档上读到更多关于这方面的内容。

这些[ __i*__]方法被调用以实现增强的算术分配(+=-=*=@=/=//=%=**=<<=+=0,+=1,+=2,+=3)。这些方法应该尝试就地执行操作(修改 self)并返回结果(可以是 self,但不一定是 self)。如果没有定义特定的方法,那么扩展赋值就会回到正常的方法。例如,如果 x 是具有 +=4方法的类的实例,则 +=5等效于 +=6。否则,考虑 +=7和 +=8,与评估 +=9一样。在某些情况下,增强的赋值可能会导致意外的错误(参见 -=1) ,但是这种行为实际上是数据模型的一部分。

在第一个示例中,您将重新分配变量 a,而在第二个示例中,您将使用 +=运算符就地修改数据。

请参阅关于 < a href = “ https://docs.python.org/3.6/reference/simple _ stmts.html # aug”> 7.2.1 :

x += 1这样的增强赋值表达式可以重写为 x = x + 1,以获得类似但不完全相同的效果。在增强版本中,x 只计算一次。而且,在可能的情况下,实际操作将就地执行,这意味着不是创建一个新对象并将其分配给目标,而是修改旧对象。

+=接线员呼叫 __iadd__。这个函数就地进行更改,只有在执行之后,结果才会被设置回您“应用”+=的对象。

另一方面,__add__ 获取参数并返回它们的和(不修改它们)。

短表单(a += 1)可以就地修改 a,而不是创建一个表示和的新对象并将其重新绑定到相同的名称(a = a + 1)。因此,短形式(a += 1)是非常有效的,因为它不一定需要像 a = a + 1那样复制 a

此外,即使它们输出相同的结果,也要注意它们是不同的,因为它们是独立的操作符: ++=

正如已经指出的,b += 1就地更新 b,而 a = a + 1计算 a + 1,然后将名称 a赋给结果(现在 a不再指代一行 A)。

为了正确理解 +=操作符,我们还需要理解 易变的永恒不变对象的概念。考虑一下当我们忽略 .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 += 1c = c + 1是等价的。这是因为现在的 C是一个1D 数组(C.ndim == 1) ,因此当在 C上迭代时,每个整数元素都被提取出来并分配给 c

现在在 Python 中,整数是不可变的,这意味着原地更新是不允许的,有效地将 c += 1转换为 c = c + 1,其中 c现在引用一个 新的整数,不以任何方式耦合到 C。当你在重新形状的数组上循环时,整行(np.ndarray)一次被分配给 b(和 a) ,它们是 易变的对象,这意味着你可以随意插入新的整数,当你插入 a += 1时就会发生这种情况。

应该提到的是,尽管 ++=应该像上面描述的那样相互关联(通常也是如此) ,但是任何类型都可以通过分别定义 __add____iadd__方法来以任意方式实现它们。

首先: 循环中的变量 a 和 b 引用 numpy.ndarray对象。

在第一个循环中,a = a + 1的计算如下: 调用 numpy.ndarray__add__(self, other)函数。这将创建一个新对象,因此不会修改 A。然后,将变量 a设置为引用结果。

在第二个循环中,不创建新对象。语句 b += 1调用 numpy.ndarray__iadd__(self, other)函数,该函数修改 b 所引用的 ndarray对象。因此,B被修改了。

这里的一个关键问题是这个循环迭代 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这样的2d 数组,我们通常不需要对行进行迭代。在单个 B上运行的许多操作也可以在整个系统上运行。B += 1B[1:] = 0等。