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

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 

OUTPUT

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

foo += bar似乎会影响类的每个实例,而foo = foo + bar似乎以我期望的方式行事。

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

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

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

所以当在一个有__iadd__定义的对象上使用+=运算符时,该对象就会被修改。 否则,它会尝试使用普通的__add__并返回一个新的对象。

这就是为什么可变types如列表+=更改对象的值,而对于不可变types(如元组,string和整数),将返回一个新对象( a += b相当于a = a + b )。

对于支持__iadd____add__types,您必须小心使用哪一个。 a += b将调用__iadd__并改变a ,而a = a + b将创build一个新对象并将其分配给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 

对于不可变的types(你没有__iadd__ ), a += ba = a + b是等价的。 这是什么让你使用+=不可变types,这似乎是一个奇怪的devise决定,直到你认为,否则你不能使用+=不可变的types,如数字!

一般情况下,请参阅Scott Griffith的答案 。 当处理像你这样的列表时, +=操作符是someListObject.extend(iterableObject)的缩写。 请参阅extend()的文档 。

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

在做foo += something你正在修改list foo ,所以你不要改变名字foo指向的引用,而是直接改变列表对象。 随着foo = foo + something ,你实际上正在创build一个新的列表。

这个示例代码将解释它:

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

请注意,当您将新列表重新分配给l时,引用如何更改。

由于bar是一个类variables而不是一个实例variables,就地修改会影响该类的所有实例。 但是当重新定义self.bar ,实例将有一个单独的实例variablesself.bar而不影响其他类实例。

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

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

foo2 ,实例variables是使用(空)class属性定义的,每个实例都有自己的bar

“正确”的实施将是:

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

当然,class级属性是完全合法的。 事实上,你可以访问和修改它们,而不需要像这样创build类的实例:

 class foo: bar = [] foo.bar = [x] 

虽然过了很多时间,说了很多正确的东西,但是没有把两种效果捆绑在一起的答案。

你有2个效果:

  1. 一个“特殊的”,也许不被注意到的列表的行为+= (如斯科特·格里菲斯所述 )
  2. 涉及类属性和实例属性的事实(如Can BerkBüder所述 )

在类foo__init__方法修改类属性。 这是因为self.bar += [x]转换为self.bar = self.bar.__iadd__([x])__iadd__()用于就地修改,所以它修改列表并返回对它的引用。

请注意,实例字典已被修改,虽然这通常不是必要的,因为类字典已经包含相同的分配。 所以这个细节几乎被忽视 – 除非你做了一个foo.bar = []之后。 这里的实例bar保持不变,这要归功于上述事实。

然而,在foo2课上,课堂上的bar被使用,但没有被触及。 相反,一个[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 

你可以使用print id(foo), id(f), id(g)来validation对象的身份()如果你使用的是Python3,不要忘记附加的() )。

顺便说一句: +=运算符被称为“增强赋值”,通常打算尽可能地进行就地修改。

其他的答案看起来似乎已经涵盖了很多,尽pipe似乎值得引用,并且提到了增强作业PEP 203 :

它们(扩充的赋值操作符)实现与它们的普通二进制forms相同的操作符,只是当左侧对象支持它时,操作是在“就地”完成的,而左侧仅被评估一次。

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

 >>> 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. class attributes and instance attributes 2. difference between the operators + and += for lists 

+运算符在列表中调用__add__方法。 它从操作数中获取所有元素,并创build一个包含维护顺序的元素的新列表。

+=运算符在列表中调用__iadd__方法。 它需要一个迭代器,并将迭代器的所有元素附加到列表中。 它不创build一个新的列表对象。

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

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

它修改了列表,并像列表方法extend

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

 self.bar = self.bar + [x] 

可以解构为:
实例没有属性bar (虽然有一个同名的类属性),所以它访问类属性bar并通过附加x来创build一个新的列表。 声明转换为:

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

然后它创build一个实例属性bar并为其分配新创build的列表。 请注意,任务的右边bar与右边的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 [] 

希望这个清除的东西。

Interesting Posts