147

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似乎以我期望的方式表现。

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

4

9 回答 9

169

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

__iadd__特殊方法用于就地添加,即它改变它作用的对象。特殊方法返回一个新对象,__add__也用于标准+运算符。

因此,当+=运算符用于已__iadd__定义的对象时,该对象将被修改到位。否则它将尝试使用普通__add__并返回一个新对象。

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

对于同时支持两者的类型,__iadd__因此__add__您必须小心使用哪一种。a += b将调用__iadd__和 mutate 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 += b并且a = a + b是等价的。这就是让您可以+=在不可变类型上使用的原因,这似乎是一个奇怪的设计决定,直到您考虑到否则您不能+=在像数字这样的不可变类型上使用!

于 2010-02-27T13:14:11.973 回答
101

对于一般情况,请参阅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,而不会影响其他类实例。

于 2010-02-27T12:19:23.760 回答
22

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

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

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

“正确”的实现是:

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

当然,类属性是完全合法的。事实上,您可以访问和修改它们,而无需像这样创建类的实例:

class foo:
    bar = []

foo.bar = [x]
于 2010-02-27T12:23:17.963 回答
9

这里涉及两件事:

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

在 classfoo2中,相反,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是类属性而不是实例属性。因此,对类属性的任何更改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 []  

希望这能解决问题。

于 2013-10-25T12:22:43.127 回答
5

其他答案似乎几乎涵盖了它,尽管它似乎值得引用和参考Augmented Assignments PEP 203

他们[增强赋值运算符]实现了与正常二进制形式相同的运算符,除了当左侧对象支持时,操作是“就地”完成的,并且左侧只计算一次。

...

Python 中增强赋值背后的想法是,它不仅是一种更简单的方法来编写将二进制运算的结果存储在其左侧操作数中的常见做法,而且还是一种将相关左侧操作数存储为知道它应该“对自己”进行操作,而不是创建自己的修改副本。

于 2013-06-12T13:23:05.623 回答
5

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

你有2个效果:

  1. 列表的“特殊”,可能未被注意到的行为+=(如Scott Griffiths所述)
  2. 涉及类属性和实例属性的事实(如Can Berk Büder 所述

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

请注意,实例 dict 已修改,尽管这通常不是必需的,因为类 dict 已经包含相同的分配。所以这个细节几乎不会被注意到——除非你foo.bar = []事后做。bar由于上述事实,这里的实例保持不变。

foo2然而,在 class中,bar使用了 class,但没有触及。相反,将 a[x]添加到其中,形成一个新对象,如此self.bar.__add__([x])处所称,它不会修改该对象。然后将结果放入实例字典中,为实例提供新列表作为字典,而类的属性保持修改。

... = ... + ...之后的分配和... += ...影响之间的区别:

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 

您可以使用验证对象的身份(如果您使用的是 Python3 ,print id(foo), id(f), id(g)请不要忘记附加的 s)。()

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

于 2012-11-11T12:56:02.980 回答
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])
于 2012-03-13T07:08:09.423 回答
1
>>> 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

另请参阅下面的 url 以了解 shallowcopy 和 deepcopy

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

于 2019-10-31T15:58:52.413 回答
-3

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

于 2020-12-17T12:38:40.377 回答