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

euc*_*lia 103 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 
Run Code Online (Sandbox Code Playgroud)

OUTPUT

[1, 2]
[1, 2]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]
[1]
[2]
Run Code Online (Sandbox Code Playgroud)

foo += bar似乎影响了类的每个实例,而foo = foo + bar似乎表现得像我期望事物的行为.

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

Sco*_*ths 113

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

__iadd__特殊方法是就地此外,也就是发生变异,它作用于对象.该__add__特殊方法返回一个新的对象,也可用于标准+操作.

因此,当+=操作符用于已__iadd__定义的对象时,对象将被修改到位.否则它将尝试使用plain __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
Run Code Online (Sandbox Code Playgroud)

对于不可变类型(您没有__iadd__)a += b并且a = a + b是等效的.这是允许你+=在不可变类型上使用的东西,这可能看起来是一个奇怪的设计决定,直到你考虑到否则你不能使用+=像数字这样的不可变类型!

  • 知道`+=` 实际上*扩展*了一个列表,这就解释了为什么`x = []; x = x + {}` 给出了一个 `TypeError` 而 `x = []; x += {}` 只返回 `[]`。 (5认同)
  • 还有一种`__radd__`方法有时可能被调用(它与大多数涉及子类的表达式相关). (4认同)
  • 透视:如果内存和速度很重要,+ =非常有用 (2认同)
  • 这个答案忽略了(非常重要的)事实:“bar”是一个类变量。这与这个答案一起实际上解释了观察到的行为。从这个意义上说,@AndiDog 的答案更好。 (2认同)

And*_*Dog 89

对于一般情况,请参阅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
Run Code Online (Sandbox Code Playgroud)

请注意重新分配新列表时参考如何更改l.

作为bar类变量而不是实例变量,在适当的位置修改将影响该类的所有实例.但是在重新定义时self.bar,实例将具有单独的实例变量,self.bar而不会影响其他类实例.

  • @ e-statis:OP清楚地谈到了列表,我明确表示我也在谈论列表.我没有概括任何东西. (11认同)
  • 这并非总是如此:a = 1; a + = 1; 是有效的Python,但是int没有任何"extend()"方法.你无法概括这一点. (7认同)
  • 做完一些测试后,斯科特·格里菲思(Scott Griffiths)做对了,所以为您-1。 (2认同)

Can*_*der 22

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

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

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

"正确"的实施将是:

class foo:
    def __init__(self, x):
        self.bar = [x]
Run Code Online (Sandbox Code Playgroud)

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

class foo:
    bar = []

foo.bar = [x]
Run Code Online (Sandbox Code Playgroud)


glg*_*lgl 5

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

你有2个效果:

  1. 一个"特殊的",也许是未被注意的列表行为+=(如Scott Griffiths所述)
  2. 涉及类属性和实例属性的事实(如CanBerkBüder所述)

在类中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 
Run Code Online (Sandbox Code Playgroud)

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

BTW:+=运营商被称为"增强分配",通常旨在尽可能地进行现场修改.


mwa*_*rdm 5

其他答案似乎几乎涵盖了它,虽然它似乎值得引用并参考增强的分配PEP 203:

它们[扩充赋值运算符]实现与它们的正常二进制形式相同的运算符,除了当左侧对象支持它时操作"就地"完成,并且左侧只被评估一次.

...

在Python中扩充赋值背后的想法是,它不仅是一种更简单的方法来编写将二进制操作的结果存储在其左侧操作数中的常见实践,而且还有一种方法可以解决左侧操作数的问题.知道它应该"自己"运行,而不是创建自己的修改副本.


aja*_*jay 5

这里涉及两件事:

1. class attributes and instance attributes
2. difference between the operators + and += for lists
Run Code Online (Sandbox Code Playgroud)

+操作员__add__在列表上调用该方法。它从其操作数中获取所有元素,并创建一个包含这些元素保持其顺序的新列表。

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

在课堂上,foo该陈述 self.bar += [x]不是作业陈述,而是实际上翻译为

self.bar.__iadd__([x])  # modifies the class attribute  
Run Code Online (Sandbox Code Playgroud)

它修改了列表并像list方法一样起作用extend

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

self.bar = self.bar + [x]  
Run Code Online (Sandbox Code Playgroud)

可以按以下方式解构:
实例没有属性bar(尽管有一个同名的类属性),因此它可以访问该类属性bar并通过附加该属性来创建新列表x。该语句转换为:

self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute 
Run Code Online (Sandbox Code Playgroud)

然后,它创建一个实例属性bar,并将新创建的列表分配给它。请注意,bar分配的rhs与bar与lhs的不同。

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

相反,类的每个实例foo2都有其自己的instance属性bar,该属性不同于同名的class属性bar

f = foo2(4)
print f.bar # accessing the instance attribute. prints [4]  
print f.__class__.bar # accessing the class attribute. prints []  
Run Code Online (Sandbox Code Playgroud)

希望这能清除一切。