Python如何递增列表元素?

19

有人能解释一下为什么第一个代码块不改变列表,而第二个代码块可以吗?

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 是一个临时变量,并不是实际引用列表中的元素的东西。不确定为什么递增它不会影响列表。


4
因为el是一个局部变量,只存在于for循环内部。 - klutt
3
不太对;for循环不会创建作用域,因此即使循环结束后,el仍然存在并且指向它绑定到的最后一个对象。 - chepner
@chepner 真的。感谢澄清。我的观点是el是每个元素的副本,而不是引用。 - klutt
8
这不是副本;它确实是对每个单独元素的引用。这些元素只是不可改变的。 - chepner
5个回答

33

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]

12

=(当左侧只有一个标识符时)纯粹是语法结构,将左侧的名称绑定到右侧的对象。

所有其他赋值都是各种方法调用的简写形式。

  • a[i] = 3a.__setitem__(i, 3) 的简写形式
  • a += 3a = a.__iadd__(3) 的简写形式
  • a[i] += 3a.__setitem__(i, a[i]+3) 的简写形式

每个方法调用的最终结果取决于 type(a) 如何实现被调用的方法。该方法可能会改变其调用者,也可能会返回一个新对象。


6
a += 3 的意思是 a = a.__iadd__(3),如果 a 是可变对象,那么方法 __iadd__ 会返回变异后的对象,但赋值操作依然发生。同样地,a[i] += 3 的意思是 a.__setitem__(i, a.__getitem__(i).__iadd__(3))。在这两种情况下,都会调用 __iadd__ 方法,并将结果重新赋值回原对象。 - Duncan

8
我最初将其写成评论,但我想稍微扩展一下,特别是加入元组的例子。 Mark Tolonen's answer 是正确的(并且已经得到了赞同),因为普通整数是不可变的(不能被更改),而列表是可变的(可以替换元素),但没有提到另外两个关键概念,在稍微恐怖的示例中会显示出来:
  1. Objects 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.

  2. "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.

  3. 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]

"means":
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),它就会不断地添加到原始列表中!
另请参见python:绑定工作原理,了解更多有关附加绑定规则的信息。:-)
正如Duncan在评论中指出 chepner的回答,在调用__iadd__之后,返回的结果会被重新绑定到对象上。(所有函数都会返回一个结果;没有表达式返回或从函数结尾"掉落",被定义为返回None。)
可变对象通常应该返回自身,而不可变对象本身就不需要实现`__iadd__`,因为在一个被认为是不可变的对象上实现变异似乎很奇怪。尽管如此,我们可以通过编写一个“假装”不可变但实际上可变的类来滥用和暴露这种行为。以下是一个例子。这并不是为了实用,只是为了说明Python的一些较黑暗的角落。
"""
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])

一旦我们创建了这两个对象并且冻结类之后,我们在o0上调用__iadd__三次,所以最终,o0o1实际上都绑定到了第二个对象。第一个对象——只能通过类的cls.instances字段找到——在其列表中有两个项目。
作为一个练习,在运行代码之前,请尝试预测它将打印出什么。
(额外高级练习:将Oddity转换为一个元类,可以应用于类以将它们转换为可冻结的多单例。[是否有一个术语来描述“类似于单例但允许N个”的事物?] 参见Why Singletons Are Controversial。)
[编辑:这一直困扰着我:原来有一个固定单例集的名称。当集合只有两个元素时,它被称为“双子集”。在Python中,True和False是bool类型的双子集。通常情况下,n个对象的推广是多重集合,经常被实例化/用作固定的哈希表——至少在“冻结”时间之后。请注意,Python的双子集布尔实例是不可变的,就像Python的单例None一样。]

2
在第一个代码块中,e1是对a中一个元素的引用(取决于我们在哪个循环迭代中)。
当你执行e1 += 5(或者更清楚地说,在这种情况下,e1 = e1 + 5),你所做的是将e1重新分配为e1 + 5的值。在此操作之后,e1不再指向列表的第一个元素,而是指向我们创建的新变量(e1 + 5)。列表保持不变。
在第二个代码块中,你正在直接为列表的一个元素赋值,而不是临时引用e1

2
在第一个例子中,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]]
在您提供的整数列表示例中,这种类型的变异是不可能的,因为整数是不可变的。所以,如果您想修改列表中的值,则必须使用计数循环方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接