【问题标题】:Why does += behave unexpectedly on lists?为什么 += 在列表中表现异常?
【发布时间】:2022-01-16 00:44:18
【问题描述】:

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 

输出

[1, 2]
[1, 2]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]
[1]
[2]

foo += bar 似乎会影响该类的每个实例,而 foo = foo + bar 的行为似乎与我期望的行为方式相同。

+= 运算符称为“复合赋值运算符”。

【问题讨论】:

  • 查看列表中“扩展”和“附加”之间的区别
  • 我不认为这表明 Python 有什么问题。大多数语言甚至不允许您在数组上使用 + 运算符。我认为在这种情况下添加+= 非常有意义。
  • 官方称它为“增强赋值”。
  • 顺便说一句,使用my_list += [x] 是一种糟糕的风格。请不要这样做

标签: python augmented-assignment


【解决方案1】:

listname.extend() 非常适合这个用途 :)

【讨论】:

    【解决方案2】:
    >>> 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 只是给了我们一个不同的对象。另一方面,我们能够对可变对象(列表)进行更改,并使其始终保持相同的对象。

    参考:https://medium.com/@tyastropheus/tricky-python-i-memory-management-for-mutable-immutable-objects-21507d1e5b95

    也可以参考下面的网址来了解浅拷贝和深拷贝

    https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/

    【讨论】:

    • # 列表的 ID 相同
    【解决方案3】:

    这里涉及到两件事:

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

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

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

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

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

    它会修改列表并像列表方法extend一样工作。

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

    self.bar = self.bar + [x]  
    

    可以解构为:
    该实例没有属性bar(虽然有一个同名的类属性),所以它访问类属性bar并通过将x附加到它来创建一个新列表。该语句翻译为:

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

    然后它创建一个实例属性bar 并将新创建的列表分配给它。请注意,赋值右侧的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 []  
    

    希望这能解决问题。

    【讨论】:

      【解决方案4】:

      对于一般情况,请参阅Scott Griffith's answer。但是,在处理像您这样的列表时,+= 运算符是someListObject.extend(iterableObject) 的简写。见documentation of extend()

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

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

      【讨论】:

      • 这并不总是正确的:a = 1;一个 += 1;是有效的 Python,但 int 没有任何“extend()”方法。你不能一概而论。
      • 做了一些测试,Scott Griffiths 做对了,所以 -1 适合你。
      • @e-statis:OP 清楚地在谈论列表,我明确表示我也在谈论列表。我没有一概而论。
      • 去掉了-1,答案很好。不过,我仍然认为格里菲斯的答案更好。
      • 对于两个列表ab,一开始认为a += ba = a + b 不同,感觉很奇怪。但这是有道理的; extend 通常是与列表有关的事情,而不是创建整个列表的新副本,这将具有更高的时间复杂度。如果开发人员需要小心不要修改原始列表,那么作为不可变对象的元组是一个更好的选择。 += 带元组不能修改原元组。
      【解决方案5】:

      其他答案似乎几乎涵盖了它,尽管它似乎值得引用并参考Augmented Assignments PEP 203

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

      ...

      增强背后的理念 Python 中的赋值是它不仅仅是一种更简单的方法来编写 将二进制运算的结果存储在其 左手操作数,也是左手操作数的一种方式 问题是要知道它应该“对自己”运行,而不是 创建自身的修改副本。

      【讨论】:

        【解决方案6】:

        虽然过去了很长时间,说了很多正确的话,但没有一个答案可以将这两种效果捆绑在一起。

        你有两种效果:

        1. 带有+= 的列表的“特殊”,可能未被注意到的行为(如Scott Griffiths 所述)
        2. 涉及类属性和实例属性的事实(如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]),它不会修改对象。然后将结果放入实例字典中,将新列表作为字典提供给实例,而类的属性保持修改。

        ... = ... + ...... += ... 之间的区别也会影响之后的分配:

        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,请不要忘记额外的 ()s)。

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

        【讨论】:

          【解决方案7】:
          >>> 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])
          

          【讨论】:

            【解决方案8】:

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

            __iadd__ 特殊方法用于就地添加,即它改变它作用的对象。 __add__ 特殊方法返回一个新对象,也用于标准 + 运算符。

            因此,当 += 运算符用于定义了 __iadd__ 的对象时,该对象将被修改到位。否则它将尝试使用普通的__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
            

            对于不可变类型(没有__iadd__a += ba = a + b 是等价的。这就是让您可以在不可变类型上使用 += 的原因,这似乎是一个奇怪的设计决定,直到您考虑到否则您不能在像数字这样的不可变类型上使用 +=

            【讨论】:

            • 还有__radd__方法有时可能会被调用(它与主要涉及子类的表达式有关)。
            • 从角度来看:如果内存和速度很重要,+= 很有用
            • 知道+= 实际上扩展 一个列表,这就解释了为什么x = []; x = x + {} 给出TypeErrorx = []; x += {} 只返回[]
            • 这个答案缺少(非常重要的)事实,即bar 是一个类变量。这与这个答案一起,实际上解释了观察到的行为。从这个意义上说,@AndiDog 的答案更好。
            【解决方案9】:

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

            foo中,类属性在init方法中被修改,所以所有实例都会受到影响。

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

            “正确”的实现是:

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

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

            class foo:
                bar = []
            
            foo.bar = [x]
            

            【讨论】:

              猜你喜欢
              • 2020-09-10
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2016-12-17
              • 2015-11-23
              • 1970-01-01
              相关资源
              最近更新 更多