Python 为什么+=在列表上的行为会出人意料?

Python 为什么+=在列表上的行为会出人意料?,python,augmented-assignment,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 prin

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 
输出

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

+=运算符称为复合赋值运算符

有关一般情况,请参见。但是,在处理像您这样的列表时,+=运算符是someListObject.ExtendedItemerableObject的缩写。看

extend函数将把参数的所有元素追加到列表中

当执行foo+=某些操作时,您正在修改列表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,而不会影响其他类实例。

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

在foo中,class属性在init方法中被修改,这就是为什么所有实例都会受到影响

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

正确的实施方式是:

class foo:
    def __init__(self, x):
        self.bar = [x]
当然,类属性是完全合法的。实际上,您可以访问和修改它们,而无需创建类的实例,如下所示:

class foo:
    bar = []

foo.bar = [x]

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

__iadd_______________________________________。__add__特殊方法返回一个新对象,也用于标准+运算符

因此,当+=运算符用于定义了uu iadd_uu的对象时,该对象将被就地修改。否则它将尝试使用普通的uu添加uu并返回一个新对象

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

对于同时支持uuu iadd_uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu。a+=b将调用uuu iadd_uuuu并对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是等价的。这是让你在不可变类型上使用+=,这似乎是一个奇怪的设计决定,直到你考虑到,否则你就不能使用不可变的类型,比如数字。p> 虽然已经过去了很多时间,说了很多正确的话,但没有一个答案能将这两种效果结合起来

>>> 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])
您有两种效果:

带+=的列表的一种特殊的、可能未被注意到的行为,如 涉及类属性和实例属性的事实,如 在类foo中,uuu init_uuu方法修改class属性。这是因为self.bar+=[x]转换为self.bar=self.bar.\uuu iadd\uuux]__iadd_uuu用于就地修改,因此它修改列表并返回对它的引用

请注意,实例dict已被修改,尽管这通常不是必需的,因为类dict已经包含相同的赋值。因此,这个细节几乎没有被注意到——除非您在之后执行foo.bar=[]。由于上述事实,这里的实例栏保持不变

但是,在foo2类中,使用了类的栏,但没有触摸。相反,一个[x]被添加到其中,形成一个新的对象,如self.bar。这里调用了\uuuu add\uuuuux],它不会修改对象。然后将结果放入实例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 idfoo、idf、idg来验证对象的身份。如果您使用的是Python3,请不要忘记附加的s


顺便说一句,+=运算符被称为增广赋值,通常是为了尽可能地进行就地修改。

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

它们[增广赋值运算符]实现相同的运算符 作为它们的标准二进制形式,除了操作已完成 `当左侧对象支撑它时,且 左边的 只有一次对边进行评估

增广后的想法 Python的任务是,它不仅仅是编写 将二进制运算的结果存储在其 左手操作数,也是中左手操作数的一种方式 问题是,它应该“自行”运行,而不是 创建自身的修改副本


这里涉及两件事:

1. class attributes and instance attributes
2. difference between the operators + and += for lists
+运算符调用列表上的uuu add_uuu方法。它从操作数中获取所有元素,并生成一个新列表,其中包含保持其顺序的元素

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

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

self.bar.__iadd__([x])  # modifies the class attribute  
它在适当的位置修改列表,其行为类似于list方法extend

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

可以解构为: 实例没有属性栏有一个同名的class属性,尽管如此,它访问class属性栏并通过向其添加x来创建一个新列表。这句话的意思是:

self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute 
然后,它创建一个实例属性栏,并将新创建的列表分配给它。请注意,作业右侧的条形图与左侧的条形图不同

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

相反,类foo2的每个实例都有自己的实例属性栏,这与同名栏的class属性不同

希望这能澄清问题

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

参考:

也请参考下面的url以了解shallowcopy和deepcopy


listname.extend在这方面非常有用:

查看列表中“extend”和“append”的区别,我不认为这表明Python有什么问题。大多数语言甚至不允许在数组上使用+运算符。我认为在这种情况下,+=将追加是非常有意义的。它被正式称为“增广赋值”。顺便说一句,使用我的列表+=[x]是一种糟糕的风格。请不要这样做这并不总是正确的:a=1;a+=1;是有效的Python,但int没有任何扩展方法。你不能一概而论。做了一些测试,斯科特·格里菲斯(Scott Griffiths)做对了,所以给你-1。@e-statis:OP显然是在谈论列表,我也清楚地说我也在谈论列表。我不是泛化任何东西。去掉-1,答案是好的。我仍然认为格里菲斯的答案更好。起初,认为a+=b与a=a+b对于两个列表a和b是不同的感觉很奇怪。但这是有道理的;扩展通常是用来处理列表的,而不是创建整个列表的新副本,因为整个列表具有更高的时间复杂度。如果开发人员需要注意不要修改原来的列表,那么元组是不可变对象的更好选择with tuples无法修改原始元组。还有一个_radd_uuu方法,有时可能会调用它,它与主要涉及子类的表达式相关。从角度来看:+=在内存和速度很重要的情况下很有用知道+=实际上扩展了一个列表,这解释了为什么x=[];x=x+{}在x=[]时给出一个TypeError;x+={}只返回[]。列表的ID相同
self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute 
f = foo2(4)
print f.bar # accessing the instance attribute. prints [4]  
print f.__class__.bar # accessing the class attribute. prints []  
>>> 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