为什么+ =在列表上意外行为?
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 += b
和a = 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个效果:
- 一个“特殊的”,也许不被注意到的列表的行为
+=
(如斯科特·格里菲斯所述 ) - 涉及类属性和实例属性的事实(如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 []
希望这个清除的东西。