为什么+ =在列表上表现异常?


118

+=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似乎以我希望事情表现的方式表现。

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


也可以在列表上看到“ extend”和“ append”之间的区别
N 1.1 2010年

3
我不认为这表明Python有问题。大多数语言甚至不允许您+在数组上使用运算符。我认为在这种情况下+=可以合理补充。
Skilldrick

4
正式地,这就是所谓的“增派任务”。
马丁·皮特斯

Answers:


138

一般的答案是+=尝试调用__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它们是等效的。这就是让您+=在不可变类型上使用的原因,这似乎是一个奇怪的设计决定,除非您考虑到否则无法+=在不可变类型(例如数字)上使用!


4
还有__radd__有时可能会被调用的方法(它与大多数涉及子类的表达式有关)。
jfs

2
透视图:如果内存和速度很重要,则+ =很有用
Norfeldt 2012年

3
知道+=实际上扩展了列表,这解释了为什么x = []; x = x + {}要花TypeError一会儿x = []; x += {}才返回[]
zezollo

96

对于一般情况,请参见Scott Griffith的答案。但是,当像您一样处理列表时,+=运算符是的简写someListObject.extend(iterableObject)。请参阅extend()文档

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而不会影响其他类实例。


7
并非总是如此:a = 1; + = 1; 是有效的Python,但int没有任何“ extend()”方法。您无法对此进行概括。
e-satis

2
做完一些测试后,斯科特·格里菲思(Scott Griffiths)做对了,所以为您-1。
e-satis

11
@ e-statis:OP显然是在谈论名单,我也明确指出我也在谈论名单。我什么也没概括。
AndiDog,2010年

删除-1,答案就足够好了。我仍然认为格里菲斯的答案更好。
e-satis

起初,觉得a += ba = a + b两个清单a和清单不同是奇怪的b。但这是有道理的;extend而不是创建整个列表的新副本(时间复杂度更高)时,通常更倾向于使用列表。如果开发人员需要注意不要修改原始列表,那么元组是不​​可变对象的更好选择。+=与元组不能修改原始元组。
Pranjal Mittal

22

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

在中foo,在init方法中修改了class属性,这就是所有实例都受影响的原因。

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

“正确”的实现将是:

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

当然,类属性是完全合法的。实际上,无需创建此类的实例即可访问和修改它们:

class foo:
    bar = []

foo.bar = [x]

8

这里涉及两件事:

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

+操作员__add__在列表上调用该方法。它从其操作数中获取所有元素,并创建一个包含这些元素保持其顺序的新列表。

+=操作员调用__iadd__列表中的方法。它需要一个iterable,并将iterable的所有元素附加到适当的列表中。它不会创建新的列表对象。

在课堂上,foo该陈述 self.bar += [x]不是作业陈述,而是实际上翻译为

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

它修改了列表并像list方法一样起作用extend

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

self.bar = self.bar + [x]  

可以按以下方式进行解构:
实例没有属性bar(尽管有一个同名的类属性),因此它可以访问该类属性bar并通过附加该属性来创建一个新列表x。该语句转换为:

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

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

对于class的实例foobar是class属性,而不是instance属性。因此,对class属性的任何更改bar都将反映在所有实例中。

相反,该类的每个实例foo2都有其自己的instance属性bar,该属性不同于同名的class属性bar

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

希望这能清除一切。


5

尽管已经过去了很多时间,并且说了许多正确的话,但是还没有答案将这两种效果捆绑在一起。

您有2种效果:

  1. 列表的一种“特殊”的,也许未被注意的行为+=(如Scott Griffiths所述
  2. 包含类属性和实例属性的事实(如Can BerkBüder所述

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

请注意,实例字典已被修改,尽管通常不需要,因为类字典已包含相同的赋值。因此,这个细节几乎没有引起注意-除非您foo.bar = []事后做。bar由于上述事实,实例在这里保持不变。

foo2但是,在class 中,使用了class 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)()如果您使用的是Python3,请不要忘记其他的)。

顺便说一句:+=运算符被称为“扩充分配”,通常旨在尽可能进行就地修改。


5

其他答案似乎几乎涵盖了所有内容,尽管它似乎值得引用和参考增值作业PEP 203

他们(增强的赋值运算符)实现了与普通二进制形式相同的运算符,不同之处在于,当左侧对象支持该操作时“就地”执行该操作,并且左侧仅被评估一次。

...

Python中的增强赋值背后的想法是,它不仅是编写将二进制运算的结果存储在其左操作数中的通用实践的简便方法,而且还是一种用于所讨论的左操作数的方法。知道它应该“自己”运行,而不是创建自己的修改副本。


1
>>> 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])

0
>>> 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/


#ID是相同的列表
罗山确定
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.