8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png

为什么 += 在列表上的行为异常?

rerson 2月前

70 0

python 中的 += 运算符似乎在列表上意外运行。有人能告诉我这是怎么回事吗?class foo: bar = [] def __init__(self,x): self.bar += [x] class

+= 似乎对列表进行了意外操作。有人能告诉我这是怎么回事吗?

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 其行为似乎符合我的预期。

我尝试用谷歌搜索这个问题,但是我不确定该 += 运营商的正式名称是什么,所以什么也没找到。

帖子版权声明 1、本帖标题:为什么 += 在列表上的行为异常?
    本站网址:http://xjnalaquan.com/
2、本网站的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
3、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
4、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
5、站长邮箱:yeweds@126.com 除非注明,本帖由rerson在本站《list》版块原创发布, 转载请注明出处!
最新回复 (0)
  • 查看列表中 'extend' 和 'append' 之间的区别

  • 一般答案是 += 尝试调用 __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 += b a = a + b 是等效的。这就是让您 += 在不可变类型上使用的原因,这似乎是一个奇怪的设计决定,直到您考虑到否则您不能 += 在数字等不可变类型上使用!

  • 还有可能有时被调用的 __radd__ 方法(​​它与主要涉及子类的表达式相关)。

  • 知道 += 实际上扩展了一个列表,这解释了为什么 x = []; x = x + {} 会出现 TypeError 而 x = []; x += {} 只返回 []。

  • 这个答案忽略了 bar 是类变量这一(非常重要)的事实。这与这个答案一起,实际上解释了观察到的行为。@AndiDog 的答案在这个意义上更好。

  • 对于一般情况,请参阅 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
    

    注意当你重新分配新列表到时引用是如何变化的 l .

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

  • 这并不总是正确的:a = 1; a += 1; 是有效的 Python,但 int 没有任何 \'extend()\' 方法。您不能概括这一点。

  • 做了一些测试,斯科特·格里菲斯 (Scott Griffiths) 的结果正确,因此你得到 -1。

  • @e-statis:原帖者显然是在谈论列表,我也明确表示我也在谈论列表。我没有泛泛而谈。

  • 删除 -1,答案已经足够好了。不过我仍然认为 Griffiths 的答案更好。

  • xin 2月前 0 只看Ta
    引用 12

    对于两个列表 a 和 b,a += b 与 a = a + b 不同,乍一看感觉很奇怪。但这是有道理的;扩展通常是对列表进行的预期操作,而不是创建整个列表的新副本,后者的时间复杂度更高。如果开发人员需要小心不要修改原始列表,那么作为不可变对象的元组是更好的选择。元组的 += 不能修改原始元组。

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

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

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

    \'正确\'的实现方式是:

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

    当然,类属性是完全合法的。事实上,你可以访问和修改它们而不需要创建类的实例,就像这样:

    class foo:
        bar = []
    
    foo.bar = [x]
    
  • 这里涉及两件事:

    1. 类属性和实例属性
    2. 列表运算符 + 和 += 之间的区别

    + 在列表上 __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 []  
    

    希望这能解决所有问题。

  • 尽管已经过去了很长时间,也说过很多正确的话,但却没有一个答案能将这两种影响结合在一起。

    你有两个效果:

    1. 一种“特殊”的、可能未被注意到的列表行为 += 所述 Scott Griffiths )
    2. 事实上,涉及类属性和实例属性(正如 Can Berk Büder )

    在类中 foo ,该 __init__ 方法修改类属性。因为 self.bar += [x] 转换为 self.bar = self.bar.__iadd__([x]) . __iadd__() 就地修改,所以它修改列表并返回对它的引用。

    请注意,实例字典已被修改,尽管这通常没有必要,因为类字典已经包含相同的分配。因此,这个细节几乎不会被注意到 - 除非你 foo.bar = [] 事后再做。由于上述事实,实例保持 bar 不变。

    在 class 中 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, () 请不要忘记附加的

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

  • 其他答案似乎已经涵盖了所有内容,但似乎值得引用和参考 增强作业 PEP 203 :

    它们 [增强赋值运算符] 实现与其正常二进制形式相同的运算符,不同之处在于当左侧对象支持时,操作是“就地”完成的,并且左侧仅被评估一次。

    ...

    Python 中增强赋值背后的想法是,它不仅是一种更简单的方法来编写将二元运算结果存储在其左侧操作数的常见做法,而且也是让左侧操作数知道它应该“对自身”进行操作而不是创建自身的修改副本的一种方法。

  • >>> 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])
    
  • >>> 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/

返回
作者最近主题: