有人能解释一下为什么第一个代码块不改变列表,而第二个代码块可以吗?
a = [1,2,3]
for el in a:
el += 5
这样便将a
设为[1,2,3]
。话虽如此,如果我执行以下操作:
a = [1,2,3]
for i in range(len(a)):
a[i] += 5
然后 a = [6,7,8]
。我的猜测是,在第一种情况下,当循环遍历元素时,el
是一个临时变量,并不是实际引用列表中的元素的东西。不确定为什么递增它不会影响列表。
Python 整数是不可变的,但列表是可变的。
在第一个示例中,el
引用不可变整数,因此 +=
创建一个新的整数,只有 el
引用它。
在第二个示例中,列表 a
直接被改变,直接修改其元素。虽然 a[0]
仍然引用不可变整数,但 +=
创建一个新的整数,但其引用直接分配给可变列表的元素。
以下是显示列表元素引用 ID 的示例。在第一个示例中,创建了新的整数,但原始列表引用未更改。
a = [1,2,3]
print [id(x) for x in a]
print a
for el in a:
el += 5 # creates new integer, but only temp name references it
print [id(x) for x in a] # references are not changed.
print a
输出
[36615248, 36615236, 36615224]
[1, 2, 3]
[36615248, 36615236, 36615224]
[1, 2, 3]
在第二种情况下,列表引用被更新:a = [1,2,3]
print [id(x) for x in a]
print a
for i in range(len(a)):
a[i] += 5 # creates new integer, but updates mutable list
print [id(x) for x in a] # references are changed.
print a
输出
[36615248, 36615236, 36615224]
[1, 2, 3]
[36615188, 36615176, 36615164]
[6, 7, 8]
=
(当左侧只有一个标识符时)纯粹是语法结构,将左侧的名称绑定到右侧的对象。
所有其他赋值都是各种方法调用的简写形式。
a[i] = 3
是 a.__setitem__(i, 3)
的简写形式a += 3
是 a = a.__iadd__(3)
的简写形式a[i] += 3
是 a.__setitem__(i, a[i]+3)
的简写形式每个方法调用的最终结果取决于 type(a)
如何实现被调用的方法。该方法可能会改变其调用者,也可能会返回一个新对象。
a += 3
的意思是 a = a.__iadd__(3)
,如果 a
是可变对象,那么方法 __iadd__
会返回变异后的对象,但赋值操作依然发生。同样地,a[i] += 3
的意思是 a.__setitem__(i, a.__getitem__(i).__iadd__(3))
。在这两种情况下,都会调用 __iadd__
方法,并将结果重新赋值回原对象。 - DuncanObjects get bound to variables.
Ordinary variable assignment like x = 3
simply binds the object on the right—which may be constructed on the spot if needed—to the name on the left.
"In-place" operators like +=
attempt to invoke modifier functions, which allows mutable objects to capture them. For instance, if x
is bound to a class instance, writing x += 3
will actually execute x.__iadd__(3)
, if x
has an __iadd__
.1 If not, it runs x = x + 3
instead, which invokes the __add__
operator: x = x.__add__(3)
. See the operator documentation for all the gory details. In this case, the objects involved—ordinary integers—don't have modifier functions:
>>> (3).__add__
<method-wrapper '__add__' of int object at 0x801c07f08>
>>> (3).__iadd__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute '__iadd__'
so this particular twist is irrelevant for int
, but it's worth remembering.
Indexed assignment, x[i] = expression
, invokes the __setitem__
method. This is how mutable (modifiable) objects mutate themselves. A list object implements __setitem__
:
>>> [].__setitem__
<method-wrapper '__setitem__' of list object at 0x8007767e8>
For completeness, I will note that it also implements __getitem__
to retrieve x[i]
.
Hence, when you write a[i] += 5
, you wind up calling:
a.__setitem__(i, a.__getitem__(i) + 5)
which is how Python manages to add 5 to the i'th element of the list bound to a
.
>>> l = [0]
>>> t = (l,)
t[0]
调用 t.__getitem__
和 t.__setitem__
。同时,t[0]
绑定到与 l
相同的列表对象。这一部分很明显:>>> t
([0],)
>>> l.append(1)
>>> t
([0, 1],)
l
进行了就地修改,因此与 l
相同的列表被命名为 t[0]
也已被修改。但现在:>>> t[0] += [2]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
([0, 1, 2],)
t
不能改变时,t
如何改变?t
并没有改变,但是列表(我们也可以通过l
访问)已经改变。列表实现了__iadd__
,因此赋值语句为:t[0] += [2]
t.__setitem__(0, t.__getitem__(0).__iadd__([2]))
__getitem__
访问了列表,__iadd__
增强了列表:
>>> l
[0, 1, 2]
t.__setitem__(0, ...)
引发了 TypeError
,但是在那之前列表已经被扩充了。l
绑定的列表对象会影响到与 t
绑定的元组对象,因为 t[0]
就是那个列表对象。这一点——变量绑定到对象上,以及数据结构(如元组、列表和字典)中的元素可以引用其他对象——对于阅读和编写 Python 代码至关重要。理解绑定规则和对象创建时机的同时,知道为什么这通常是一个坏主意,是掌握此技能的关键。def f(a=[]):
f
内部添加到列表中(例如使用a.append
),它就会不断地添加到原始列表中!
__iadd__
之后,返回的结果会被重新绑定到对象上。(所有函数都会返回一个结果;没有表达式返回或从函数结尾"掉落",被定义为返回None
。)"""
demonstration of iadd behavior
"""
from __future__ import print_function
class Oddity(object):
"""
A very odd class: like a singleton, but there can be
more than one of them. Each one is a list that just
accumulates more items. The __iadd___ (+=) operator
augments the item, then advances to the next instance.
Creating them is tricky as we want to create new ones
up until we "freeze" the class, then start re-using
the instances. We use a __new__ operator a la immutable
objects, plus a boolean in the class itself, even though
each instance is mutable.
"""
def __new__(cls):
if not hasattr(cls, 'frozen'):
cls.frozen = False
if cls.frozen:
whichone = cls.rotator
cls.rotator = (whichone + 1) % len(cls.instances)
return cls.instances[whichone]
self = object.__new__(cls)
if not hasattr(cls, 'instances'):
cls.instances = []
self.whichone = len(cls.instances)
self.values = []
cls.instances.append(self)
print('created', self)
return self
def __str__(self):
return '#{}, containing {}'.format(self.whichone, self.values)
def __iadd__(self, value):
print('iadd to', self)
self.values.append(value)
all_oddities = self.__class__.instances
nextone = (self.whichone + 1) % len(all_oddities)
return all_oddities[nextone]
@classmethod
def freeze(cls):
if not hasattr(cls, 'frozen'):
raise TypeError('need at least one instance to freeze')
cls.frozen = True
cls.rotator = 0
# Now make two instances, and freeze the rest so that
# we can cycle back and forth.
o0 = Oddity()
o1 = Oddity()
Oddity.freeze()
print('o0 is', o0)
o0 += 'first add to o0'
o0 += 'second add to o0'
o0 += 'third add to o0'
print('now o0 is', o0, 'and o1 is', o1)
print('the first object is', Oddity.instances[0])
__iadd__
三次,所以最终,o0
和o1
实际上都绑定到了第二个对象。第一个对象——只能通过类的cls.instances
字段找到——在其列表中有两个项目。Oddity
转换为一个元类,可以应用于类以将它们转换为可冻结的多单例。[是否有一个术语来描述“类似于单例但允许N个”的事物?] 参见Why Singletons Are Controversial。)e1
是对a
中一个元素的引用(取决于我们在哪个循环迭代中)。e1 += 5
(或者更清楚地说,在这种情况下,e1 = e1 + 5
),你所做的是将e1
重新分配为e1 + 5
的值。在此操作之后,e1
不再指向列表的第一个元素,而是指向我们创建的新变量(e1 + 5
)。列表保持不变。e1
。el
是一个局部变量,它完全不知道它是列表的一部分。+=
的简写,并使用可变对象的列表而不是不可变整数,则这里的教训将更加清晰:a = [[1], [2]]
for el in a:
el = [3]
在这个循环之后,a
没有改变:
>>> a
[[1], [2]]
计数循环模式:
for i in range(len(a)):
a[i] = [3]
在此循环后,a
已经被改变:
>>> a
[[3], [3]]
另一方面,如果您突变el
本身而不是重新分配它:a = [[1], [2]]
for el in a:
el[0] = el[0] + 5
el
,而是在不重新指定的情况下对其进行突变。
>>> a
[[6], [7]]
在您提供的整数列表示例中,这种类型的变异是不可能的,因为整数是不可变的。所以,如果您想修改列表中的值,则必须使用计数循环方法。
for
循环不会创建作用域,因此即使循环结束后,el
仍然存在并且指向它绑定到的最后一个对象。 - chepner