关于python:为什么+=在列表中的行为异常?

Why does += behave unexpectedly on lists?

python中的+=操作符似乎在列表中意外地操作。有人能告诉我这里发生了什么事吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
3
4
5
6
7
8
[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__并返回一个新对象。

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

对于同时支持__iadd____add__的类型,因此必须小心使用哪一种。a += b将调用__iadd__并使a变异,而a = a + b将创建一个新对象并将其分配给a。他们的行动不一样!

1
2
3
4
5
6
7
8
>>> 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是等效的。这就是让您在不可变类型上使用+=的原因,在您考虑不可变类型(如数字)之前,这可能看起来是一个奇怪的设计决定!


有关一般情况,请参阅Scott Griffith的回答。但是,在处理像您这样的列表时,+=操作符是someListObject.extend(iterableObject)的缩写。请参见extend()的文档。

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

在执行foo += something操作时,您将在适当的位置修改列表foo,因此不会更改名称foo指向的引用,而是直接更改列表对象。使用foo = foo + something,您实际上正在创建一个新的列表。

此示例代码将解释它:

1
2
3
4
5
6
7
8
9
>>> l = []
>>> id(l)
13043192
>>> l += [3]
>>> id(l)
13043192
>>> l = l + [3]
>>> id(l)
13059216

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

由于bar是类变量而不是实例变量,因此就地修改将影响该类的所有实例。但在重新定义self.bar时,实例将有一个单独的实例变量self.bar而不影响其他类实例。


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

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

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

"正确"的实施将是:

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

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

1
2
3
4
class foo:
    bar = []

foo.bar = [x]

其他答案似乎已经涵盖了很多内容,但似乎值得引用和参考的是增强型作业PEP 203:

They [the augmented assignment operators] implement the same operator
as their normal binary form, except that the operation is done
`in-place' when the left-hand side object supports it, and that the
left-hand side is only evaluated once.

The idea behind augmented
assignment in Python is that it isn't just an easier way to write the
common practice of storing the result of a binary operation in its
left-hand operand, but also a way for the left-hand operand in
question to know that it should operate `on itself', rather than
creating a modified copy of itself.


虽然已经过了很多时间,说了很多正确的话,但没有答案能将两种影响捆绑在一起。

你有两种效果:

  • 使用EDOCX1[0]的"特殊"的、可能不被注意的列表行为(如scott griffiths所述)
  • 涉及类属性和实例属性的事实(如can berk b_der所述)
  • 在类foo中,__init__方法修改类属性。这是因为self.bar += [x]翻译成self.bar = self.bar.__iadd__([x])__iadd__()用于就地修改,因此它修改列表并返回对它的引用。

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

    然而,在foo2类中,使用了该类的bar,但未被触摸。相反,它添加了一个[x],形成了一个新的对象,这里称为self.bar.__add__([x]),它不修改对象。然后将结果放入实例dict中,将新列表作为dict提供给实例,同时类的属性保持修改状态。

    ... = ... + ...... += ...之间的区别影响以及随后的分配:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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,请不要忘记附加的())。

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


    这里涉及两件事:

    1
    2
    1. class attributes and instance attributes
    2. difference between the operators + and += for lists

    +运算符调用列表上的__add__方法。它从操作数中提取所有元素,并创建一个新列表,其中包含保持其顺序的元素。

    +=运算符调用列表上的__iadd__方法。它需要一个iterable并将iterable的所有元素附加到列表中。它不会创建新的列表对象。

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

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

    它在适当的位置修改列表,并像list方法extend那样工作。

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

    1
    self.bar = self.bar + [x]

    可以解构为:该实例没有属性bar(尽管有一个同名的类属性),因此它访问类属性bar,并通过附加x创建一个新列表。该声明翻译为:

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

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

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

    相反,类foo2的每个实例都有自己的实例属性bar,这与同一名称bar的类属性不同。

    1
    2
    3
    f = foo2(4)
    print f.bar # accessing the instance attribute. prints [4]  
    print f.__class__.bar # accessing the class attribute. prints []

    希望这能解决问题。


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    >>> 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])