Python如何增加列表元素?

Nat*_*men 14 python memory

有人可以向我解释为什么第一个代码块不会更改列表,但第二个代码块会更改列表.

a = [1,2,3]
for el in a:
    el += 5
Run Code Online (Sandbox Code Playgroud)

这留下a[1,2,3].如果我跑,那就说

a = [1,2,3]
for i in range(len(a)):
    a[i] += 5
Run Code Online (Sandbox Code Playgroud)

然后a = [6,7,8].我的猜测是,在第一个,循环元素el是一个临时变量,而不是实际上引用列表中的元素.不知道为什么递增它不会影响列表.

Mar*_*nen 29

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
Run Code Online (Sandbox Code Playgroud)

产量

[36615248, 36615236, 36615224]
[1, 2, 3]
[36615248, 36615236, 36615224]
[1, 2, 3]
Run Code Online (Sandbox Code Playgroud)

在第二种情况下,列表引用更新:

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
Run Code Online (Sandbox Code Playgroud)

产量

[36615248, 36615236, 36615224]
[1, 2, 3]
[36615188, 36615176, 36615164]
[6, 7, 8]
Run Code Online (Sandbox Code Playgroud)


che*_*ner 10

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

所有其他分配都是各种方法调用的简写.

  • 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__`并且在这两种情况下都有转回原文的作业. (5认同)

tor*_*rek 7

我首先把它写成评论,但我想稍微扩展它,特别是添加元组示例.

Mark Tolonen的答案是正确的(并且是upvoted),因为普通的整数是不可变的(不能改变),列表是可变的(可以替换元素),但是没有提到另外几个关键概念,这些概念出现在一些有些可怕的例子中:

  1. 对象获得约束变量.

    普通的变量赋值就像x = 3简单地绑定右边的对象 - 如果需要可以在现场构造 - 到左边的名称.

  2. "就地"操作符就像+=尝试调用修饰符函数一样,它允许可变对象捕获它们.例如,如果x绑定到类实例,则写入x += 3将实际执行x.__iadd__(3),如果x__iadd__.1 如果不是,则运行x = x + 3它,调用__add__运算符:x = x.__add__(3).有关所有血腥细节,请参阅操作员文档.在这种情况下,涉及的对象 - 普通整数 - 没有修饰函数:

    >>> (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__'
    
    Run Code Online (Sandbox Code Playgroud)

    所以这种特殊的扭曲是无关紧要的int,但值得记住.

  3. 索引赋值,x[i] = expression调用该__setitem__方法.这是可变(可修改)对象变异的方式.列表对象实现__setitem__:

    >>> [].__setitem__
    <method-wrapper '__setitem__' of list object at 0x8007767e8>
    
    Run Code Online (Sandbox Code Playgroud)

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

    因此,当你写作时a[i] += 5,你最终会打电话:

    a.__setitem__(i, a.__getitem__(i) + 5)
    
    Run Code Online (Sandbox Code Playgroud)

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

这是一个有点可怕的例子.一个元组对象是不是修改,而是一个列表对象.如果我们将列表嵌入到元组中:

>>> l = [0]
>>> t = (l,)
Run Code Online (Sandbox Code Playgroud)

然后我们可以t[0]用来调用t.__getitem__t.__setitem__.同时t[0]绑定到同一个列表对象l.这部分很明显:

>>> t
([0],)
>>> l.append(1)
>>> t
([0, 1],)
Run Code Online (Sandbox Code Playgroud)

我们进行了修改l,因此修改t[0]了相同列表的名称l.但现在:

>>> 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],)
Run Code Online (Sandbox Code Playgroud)

什么?!怎么t改变,当我们得到一个错误告诉我们,t不能改变?

答案是,t 没有改变,但列表(我们也可以访问l)确实发生了变化.列表实现__iadd__,所以赋值:

t[0] += [2]
Run Code Online (Sandbox Code Playgroud)

"手段":

t.__setitem__(0, t.__getitem__(0).__iadd__([2]))
Run Code Online (Sandbox Code Playgroud)

__getitem__访问列表和__iadd__增强列表:

>>> l
[0, 1, 2]
Run Code Online (Sandbox Code Playgroud)

然后t.__setitem__(0, ...)提升了TypeError,但是那个名单已经增加了.

请注意,顺便说一下,调整绑定的列表对象l会影响绑定的元组对象t,因为这t[0] 列表对象.事实上,变量绑定到对象,数据结构中的元素(如元组,列表和字典)可以引用其他对象 - 这对于读取和编写Python代码至关重要. 理解绑定规则以及何时创建对象是了解为什么这通常是一个坏主意的关键:

def f(a=[]):
Run Code Online (Sandbox Code Playgroud)

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


另请参阅python:如何绑定对于其他绑定规则的更多工作(方式).:-)

1正如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])
Run Code Online (Sandbox Code Playgroud)

一旦我们创建了两个对象和冷冻类,我们调用__iadd__上三次o0,所以最后,o0o1实际上都绑定到第二个对象.第一个只能通过类的cls.instances字段找到的对象在其列表项中有两个项目.

作为练习,在运行之前尝试预测会打印出来的内容.

(超高级练习:Oddity变成一个可以应用于类的类,将它们变成可冻结的多单体.[是否有一个术语表示" 单身但是允许其中的N个"?]参见为什么单身人士有争议.)

[编辑:这已被窃听我:原来有用于固定设置的,单身的名称.当集合只有两个元素时,它就是"双重".在Python,TrueFalse是包括在doubletons bool类型.对n个对象的推广是多重的,经常被实例化/用作固定 - 至少在"冻结"时间哈希表之后.请注意,Python的doubleton布尔实例是不可变的,就像Python的单例一样None.