关于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 += b和a = 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])