【问题标题】:How does Python increment list elements?Python如何增加列表元素?
【发布时间】:2017-03-12 15:59:44
【问题描述】:

谁能解释一下为什么第一个代码块不会改变列表,而第二个会。

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 是一个临时变量,而不是实际上引用列表中该元素的东西。不知道为什么增加它不会影响列表。

【问题讨论】:

  • 因为el是局部变量,只存在于for循环中。
  • @klutt 不完全; for 循环没有建立作用域,所以el 在循环退出后仍然存在,并且指向它绑定的最后一个对象。
  • @chepner 是的。感谢您的澄清。我的意思是 el 是一个副本,而不是对每个元素的引用。
  • 不是副本;它对每个单独元素的引用。元素是不可变的。
  • 天哪。谢谢。

标签: python memory


【解决方案1】:

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]

【讨论】:

    【解决方案2】:

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

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

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

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

    【讨论】:

    • a += 3a = a.__iadd__(3) 的缩写。当a 是可变的时,__iadd__ 方法返回突变的对象,但赋值仍然发生。同样,a[i] += 3a.__setitem__(i, a.__getitem__(i).__iadd__(3)) 的缩写,在这两种情况下都会调用 __iadd__,并且在这两种情况下都会有一个返回原始值的赋值。
    【解决方案3】:

    我最初是作为评论写的,但我想稍微扩展一下,特别是添加元组示例。

    Mark Tolonen's answer 是正确的(并且赞成),因为普通整数是不可变的(不能更改),而列表是可变的(可以替换元素),但没有提到另外几个关键概念,这些概念有点吓人例子:

    1. 对象绑定到变量。

      x = 3 这样的普通变量赋值只是将右侧的对象(如果需要可以在现场构造)绑定到左侧的名称。

    2. += 这样的“就地”运算符会尝试调用修饰函数,从而允许可变对象捕获它们。例如,如果x 绑定到一个类实例,写入x += 3 将实际执行x.__iadd__(3),如果x 有一个__iadd__1 如果没有,它运行@987654335 @ 而是调用 __add__ 运算符:x = x.__add__(3)。有关所有血腥细节,请参阅the operator documentation。在这种情况下,所涉及的对象——普通整数——没有修饰函数:

      >>> (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__'
      

      所以这个特殊的转折与int 无关,但值得记住。

    3. 索引 赋值 x[i] = expression 调用 __setitem__ 方法。这就是可变(可修改)对象自身发生变异的方式。列表对象实现__setitem__

      >>> [].__setitem__
      <method-wrapper '__setitem__' of list object at 0x8007767e8>
      

      为了完整起见,我会注意到它还实现了__getitem__检索 x[i]

      因此,当您写 a[i] += 5 时,您最终会调用:

      a.__setitem__(i, a.__getitem__(i) + 5)
      

      这就是 Python 如何设法将 5 添加到绑定到 a 的列表的第 i 个元素中。

    这是一个有点吓人的例子。 tuple 对象是不可 可修改的,但 list 对象是可修改的。如果我们将列表嵌入到元组中:

    >>> 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=[]):
    

    具体来说,这里的列表对象是在def 时间创建的,即只有一次。任何时候有人添加到f 中的列表,例如a.append,它会不断添加到原始列表中!


    另请参阅python: how binding works,了解更多关于附加绑定规则的信息。 :-)

    1作为Duncan points out in a commentchepner's answer,调用__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实际上都绑定到了秒 em>对象。第一个对象(只能通过类的 cls.instances 字段找到)在其项目列表中有两个项目。

    作为一个练习,在运行之前尝试预测它会打印出什么。

    (超高级练习:将Oddity 变成一个元类,可以将它们应用于类以将它们变成可冻结的多单件。[是否有“事物是喜欢一个单例,但允许其中 N 个“?] 另见 Why Singletons Are Controversial。)

    [编辑:这一直困扰着我:原来一个固定集合的单例的名称。当集合只有两个元素时,它是一个“doubleton”。在 Python 中,TrueFalse 是组成 bool 类型的双元组。 n 个对象的泛化是multiton,经常被实例化/用作固定的——至少在“冻结”时间之后——哈希表。请注意,Python 的 doubleton 布尔实例是不可变的,Python 的单例 None 也是如此。]

    【讨论】:

      【解决方案4】:

      在第一个代码块中,e1 是对a 中的一个元素的引用(取决于我们所处的循环迭代)。

      当您执行e1 += 5(或更清楚地在本例中为e1 = e1 + 5)时,您所做的是重新分配 e1e1 + 5 的值。在这个操作之后,e1 不再指向列表的第一个元素,而是我们创建的新变量(e1 + 5)。列表保持不变。

      在第二个代码块中,您将某些内容直接分配给列表的元素,而不是临时引用 e1

      【讨论】:

        【解决方案5】:

        在第一个示例中,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]]
        

        如果您的原始示例包含整数列表, 这种突变是不可能的, 因为整数是不可变的。 所以如果你想修改列表中的值, 您必须使用计数循环方法。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-02-25
          • 1970-01-01
          • 1970-01-01
          • 2021-07-04
          • 2021-12-28
          • 2022-06-24
          • 1970-01-01
          相关资源
          最近更新 更多