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

2024-11-22 08:47:00
admin
原创
146
摘要:问题描述:python 中的运算符+=似乎对列表进行了意外操作。有人能告诉我这是怎么回事吗?class foo: bar = [] def __init__(self,x): self.bar += [x] class foo2: bar = [] ...

问题描述:

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

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


解决方案 1:

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

解决方案 2:

对于一般情况,请参阅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而不会影响其他类实例。

解决方案 3:

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

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

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

“正确”的实现方式是:

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

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

class foo:
    bar = []

foo.bar = [x]

解决方案 4:

这里涉及两件事:

  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左侧的分配不同。

对于类的实例foobar是类属性而不是实例属性。因此对类属性的任何更改都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 []  

希望这能解决所有问题。

解决方案 5:

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

你有两个效果:

  1. 一种“特殊的”,可能未被注意到的列表行为(正如Scott Griffiths+=所述)

  2. 事实上,涉及类属性以及实例属性(正如Can Berk Büder所述)

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

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

foo2然而,在 class 中,类的属性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 

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

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

解决方案 6:

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

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

...

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

解决方案 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:

>>> 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/

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用