为什么 += 在列表上的行为异常?
- 2024-11-22 08:47:00
- admin 原创
- 146
问题描述:
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 += b
和a = 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:
这里涉及两件事:
类属性和实例属性
列表中运算符 + 和 += 之间的区别
+
运算符在列表上调用该__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 []
希望这能解决所有问题。
解决方案 5:
尽管已经过去了很长时间,也说过很多正确的话,但却没有一个答案能将这两种影响结合在一起。
你有两个效果:
一种“特殊的”,可能未被注意到的列表行为(正如Scott Griffiths
+=
所述)事实上,涉及类属性以及实例属性(正如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/