为什么+ =在列表上出现意外行为?

时间:2010-02-27 12:16:00

标签: python augmented-assignment

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似乎表现得像我期望事物的行为。

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

9 个答案:

答案 0 :(得分:109)

一般答案是+=尝试调用__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是等效的。这是允许你在不可变类型上使用+=的东西,这可能看起来是一个奇怪的设计决定,除非你考虑到否则你不能在数字等不可变类型上使用+=

答案 1 :(得分:89)

对于一般情况,请参阅Scott Griffith's answer。但是,在处理类似于您的列表时,+=运算符是someListObject.extend(iterableObject)的简写。请参阅documentation of 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,而不会影响其他类实例。

答案 2 :(得分:22)

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

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

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

“正确”的实施将是:

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

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

class foo:
    bar = []

foo.bar = [x]

答案 3 :(得分:5)

虽然已经过了很多时间并且说了许多正确的事情,但没有任何答案能够兼顾两种效果。

你有两种效果:

  1. 带有+=的列表的“特殊”,可能未被注意的行为(如Scott Griffiths所述)
  2. 涉及类属性和实例属性的事实(如Can Berk Büder所述)
  3. 在课程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)验证对象的身份(如果您使用的是Python3,请不要忘记其他()。)

    BTW:+=运算符称为“扩充赋值”,通常用于尽可能进行原位修改。

答案 4 :(得分:5)

其他答案似乎几乎涵盖了它,虽然它似乎值得引用并引用Augmented Assignments PEP 203

  

他们 [扩充的赋值运算符] 实现相同的运算符   作为正常的二进制形式,除了操作完成   当左侧物体支撑它时,就地“,就地”   左侧只评估一次。

...

  

背后的想法得到了增强   Python中的赋值是它不仅仅是一种更简单的编写方式   将二进制运算的结果存储在其中的常见做法   左手操作数,也是左手操作数的一种方式   问题是要知道它应该“自己”运行,而不是   创建自己的修改副本。

答案 5 :(得分:3)

这里涉及两件事:

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  

可以修改列表,就像列表方法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并为其分配新创建的列表。请注意,赋值的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 []  

希望这可以解决问题。

答案 6 :(得分: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])

答案 7 :(得分: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只是给了我们一个不同的对象。另一方面,我们可以对可变对象(列表)进行更改,并使其始终保持不变。

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

也请参考以下网址以了解浅拷贝和深拷贝

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

答案 8 :(得分:-2)

listname.extend()为此非常有用:)