为什么 + = 在列表中表现得出乎意料?

Python 中的 +=操作符似乎在列表上意外地进行操作。有人能告诉我这是怎么回事吗?

class foo:
bar = []
def __init__(self,x):
self.bar += [x]




class foo2:
bar = []
def __init__(self,x):
self.bar = self.bar + [x]


f = foo(1)
g = foo(2)
print f.bar
print g.bar


f.bar += [3]
print f.bar
print g.bar


f.bar = f.bar + [4]
print f.bar
print g.bar


f = foo2(1)
g = foo2(2)
print f.bar
print g.bar

输出

[1, 2]
[1, 2]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]
[1]
[2]

foo += bar似乎影响类的每个实例,而 foo = foo + bar似乎按照我期望的方式运行。

+=运算符称为“复合赋值运算符”。

106160 次浏览

有关一般情况,请参见 Scott Griffith 的回答。但是,当像您这样处理列表时,+=操作符是 someListObject.extend(iterableObject)的简写。看看 扩展文档()

extend函数将参数的所有元素追加到列表中。

在执行 foo += something时,您正在适当地修改列表 foo,因此您不会更改名称 foo指向的引用,但是您将直接更改列表对象。使用 foo = foo + something,您实际上是在创建一个 新的列表。

下面的示例代码将对此进行解释:

>>> l = []
>>> id(l)
13043192
>>> l += [3]
>>> id(l)
13043192
>>> l = l + [3]
>>> id(l)
13059216

注意当您将新列表重新分配给 l时引用是如何变化的。

由于 bar是一个类变量而不是一个实例变量,因此适当的修改将影响该类的所有实例。但是,当重新定义 self.bar时,该实例将有一个单独的实例变量 self.bar,而不会影响其他类实例。

这里的问题是,bar被定义为 class 属性,而不是实例变量。

foo中,class 属性在 init方法中被修改,这就是为什么所有实例都受到影响。

foo2中,使用(空) class 属性定义一个实例变量,每个实例都有自己的 bar

“正确”的实施方式是:

class foo:
def __init__(self, x):
self.bar = [x]

当然,类属性是完全合法的。实际上,您可以访问和修改它们,而无需创建类的实例,如下所示:

class foo:
bar = []


foo.bar = [x]

一般的答案是,+=尝试调用 __iadd__特殊方法,如果这个方法不可用,则尝试使用 __add__。所以问题在于这些特殊方法之间的区别。

__iadd__的特殊方法用于就地添加,也就是说,它对它所作用的对象进行变异。__add__特殊方法返回一个新对象,也用于标准的 +操作符。

因此,当 +=操作符用于定义了 __iadd__的对象时,该对象就会被修改到位。否则,它将尝试使用纯 __add__并返回一个新对象。

这就是为什么对于像列表这样的可变类型,+=改变了对象的值,而对于像元组、字符串和整数这样的不可变类型,返回的是一个新的对象(a += b变得等价于 a = a + b)。

因此,对于同时支持 __iadd____add__的类型,必须小心选择使用哪一种。a += b将调用 __iadd__并变异 a,而 a = a + b将创建一个新对象并将其分配给 a。他们不是同一个行动!

>>> a1 = a2 = [1, 2]
>>> b1 = b2 = [1, 2]
>>> a1 += [3]          # Uses __iadd__, modifies a1 in-place
>>> b1 = b1 + [3]      # Uses __add__, creates new list, assigns it to b1
>>> a2
[1, 2, 3]              # a1 and a2 are still the same list
>>> b2
[1, 2]                 # whereas only b1 was changed

对于不可变类型(没有 __iadd__的类型) ,a += ba = a + b是等价的。这就是允许您在不可变类型上使用 +=的原因,这可能看起来是一个奇怪的设计决定,直到您考虑到否则您不能在数字等不可变类型上使用 +=

>>> elements=[[1],[2],[3]]
>>> subset=[]
>>> subset+=elements[0:1]
>>> subset
[[1]]
>>> elements
[[1], [2], [3]]
>>> subset[0][0]='change'
>>> elements
[['change'], [2], [3]]


>>> a=[1,2,3,4]
>>> b=a
>>> a+=[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
>>> a=[1,2,3,4]
>>> b=a
>>> a=a+[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4])

虽然已经过去了很长时间,说了很多正确的事情,但是没有答案能说明是哪一个捆绑了这两种效应。

你有两个效果:

  1. 一个“特殊的”,也许不被注意的行为列表与 +=(如所述的 Scott Griffiths)
  2. 涉及类属性和实例属性的事实(如 可以伯克比德所述)

在类 foo中,__init__方法修改 class 属性。这是因为 self.bar += [x]翻译成 self.bar = self.bar.__iadd__([x])__iadd__()用于替代修改,因此它修改列表并返回对它的引用。

请注意,虽然这通常不是必需的,但是实例 dict 会被修改,因为类 dict 已经包含了相同的赋值。因此,这个细节几乎没有引起注意-除非你做一个 foo.bar = []之后。由于上述事实,这里实例的 bar保持不变。

然而,在类 foo2中,使用了类的 bar,但是没有触及。相反,一个 [x]被添加到它,形成一个新的对象,这里称为 self.bar.__add__([x]),它不修改对象。然后,将结果放入实例 dict 中,将新列表作为 dict 提供给实例,而类的属性保持修改状态。

... = ... + ...... += ...之间的区别也影响到以后的作业:

f = foo(1) # adds 1 to the class's bar and assigns f.bar to this as well.
g = foo(2) # adds 2 to the class's bar and assigns g.bar to this as well.
# Here, foo.bar, f.bar and g.bar refer to the same object.
print f.bar # [1, 2]
print g.bar # [1, 2]


f.bar += [3] # adds 3 to this object
print f.bar # As these still refer to the same object,
print g.bar # the output is the same.


f.bar = f.bar + [4] # Construct a new list with the values of the old ones, 4 appended.
print f.bar # Print the new one
print g.bar # Print the old one.


f = foo2(1) # Here a new list is created on every call.
g = foo2(2)
print f.bar # So these all obly have one element.
print g.bar

您可以使用 print id(foo), id(f), id(g)验证对象的标识(如果您使用 Python 3,请不要忘记附加的 ())。

顺便说一下: +=操作符被称为“增强赋值”,通常用于尽可能地替代修改。

其他答案似乎基本涵盖了这个问题,不过似乎值得引用和参考 增加的任务 PEP 203:

它们 [扩充赋值运算符]实现相同的操作符 作为它们的正常二进制形式,除非操作已经完成 当左边的对象支持它时,“就位”,并且 左边只计算一次。

...

背后的想法增强了 在 Python 中的赋值是,它不仅仅是一种更简单的方法来编写 储存二元运算结果的一般做法 左操作数,也是左操作数的一种方法 问题是知道它应该“自己运作”,而不是 创建自身的修改副本。

这里涉及到两件事:

1. class attributes and instance attributes
2. difference between the operators + and += for lists

+操作符调用列表上的 __add__方法。它从其操作数中取出所有元素,并创建一个新列表,其中包含维持其顺序的元素。

+=操作符调用列表上的 __iadd__方法。它接受一个迭代器,并将迭代器的所有元素追加到适当的列表中。它不创建新的列表对象。

在类 foo中,语句 self.bar += [x]不是赋值语句,而是实际转换为

self.bar.__iadd__([x])  # modifies the class attribute

它在适当的位置修改列表,并且类似于列表方法 extend

相反,在类 foo2中,init方法中的赋值语句

self.bar = self.bar + [x]

可以解构为:
该实例没有属性 bar(但是有一个同名的 class 属性) ,因此它访问 class 属性 bar并通过将 x附加到它来创建一个新列表。该声明翻译过来就是:

self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute

然后它创建一个实例属性 bar,并将新创建的列表分配给它。注意,赋值的 rhs 上的 bar与 lhs 上的 bar不同。

对于类 foo的实例,bar是类属性而不是实例属性。因此,对类属性 bar的任何更改都将反映给所有实例。

相反,类 foo2的每个实例都有自己的实例属性 bar,这与同名的类属性 bar不同。

f = foo2(4)
print f.bar # accessing the instance attribute. prints [4]
print f.__class__.bar # accessing the class attribute. prints []

希望这能让你清醒。

>>> a = 89
>>> id(a)
4434330504
>>> a = 89 + 1
>>> print(a)
90
>>> id(a)
4430689552  # this is different from before!


>>> test = [1, 2, 3]
>>> id(test)
48638344L
>>> test2 = test
>>> id(test)
48638344L
>>> test2 += [4]
>>> id(test)
48638344L
>>> print(test, test2)  # [1, 2, 3, 4] [1, 2, 3, 4]```
([1, 2, 3, 4], [1, 2, 3, 4])
>>> id(test2)
48638344L # ID is different here


我们可以看到,当我们试图修改一个不可变物件(在本例中是整数)时,Python 只是给了我们一个不同的对象。另一方面,我们能够对一个可变对象(一个列表)进行更改,并让它始终保持相同的对象。

档号: https://medium.com/@tyastropheus/tricky-python-i-memory-management-for-mutable-immutable-objects-21507d1e5b95

也参考下面的网址,以了解浅拷贝和深拷贝

Https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/

listname.extend()非常适合这个目的:)