为什么更改元组中的列表会引发异常,但仍然会对其进行更改?

时间:2020-06-27 18:38:07

标签: python python-3.x tuples immutability

我不确定我是否完全理解下面的迷你代码片段(在Py v3.6.7上)发生了什么。即使有人向我解释一下如何成功更改列表(即使Python引发了错误),也很棒。

我知道我们可以更改列表并对其进行更新,但是出现了什么错误?就像我给人的印象是,如果出现错误,则x应该保持不变。

x = ([1, 2], )
x[0] += [3,4] # ------ (1)

在第(1)行中引发的回溯是

> TypeError: 'tuple' object doesn't support item assignment.. 

我了解错误的含义,但无法获取该错误的上下文。

但是现在,如果我尝试打印变量x的值,Python会说是,

print(x) # returns ([1, 2, 3, 4])

据我所知,Python允许列表发生突变后发生了异常,然后希望它尝试将其重新分配回去。我认为元组是不可变的。

有人可以解释幕后发生的事情吗?

编辑-1 来自ipython控制台的图像错误;

ipython-image

3 个答案:

答案 0 :(得分:9)

我的直觉是,行x[0] += [3, 4]首先会修改列表本身,因此[1, 2]会变成[1, 2, 3, 4],然后然后尝试调整元组的内容会抛出TypeError,但是元组始终指向同一列表,因此在指向 的对象被修改时,其内容(根据指针)不会被修改。

我们可以这样验证:

a_list = [1, 2, 3]
a_tuple = (a_list,)
print(a_tuple)
>>> ([1, 2, 3],)

a_list.append(4)
print(a_tuple)
>>> ([1, 2, 3, 4], )

即使存储在“不可变”元组中,它也不会引发错误并会对其进行修改。

答案 1 :(得分:9)

这里发生了一些事情。

+=并不总是+,然后总是=

+=+可以有不同的实现方式。

看看这个例子。

In [13]: class Foo: 
    ...:     def __init__(self, x=0): 
    ...:         self.x = x 
    ...:     def __add__(self, other): 
    ...:         print('+ operator used') 
    ...:         return Foo(self.x + other.x) 
    ...:     def __iadd__(self, other): 
    ...:         print('+= operator used') 
    ...:         self.x += other.x 
    ...:         return self 
    ...:     def __repr__(self): 
    ...:         return f'Foo(x={self.x})' 
    ...:                                                                        

In [14]: f1 = Foo(10)                                                           

In [15]: f2 = Foo(20)                                                           

In [16]: f3 = f1 + f2                                                           
+ operator used

In [17]: f3                                                                     
Out[17]: Foo(x=30)

In [18]: f1                                                                     
Out[18]: Foo(x=10)

In [19]: f2                                                                     
Out[19]: Foo(x=20)

In [20]: f1 += f2                                                               
+= operator used

In [21]: f1                                                                     
Out[21]: Foo(x=30)

类似地,列表类具有++=的单独实现。

实际上,使用+=在后​​台执行了extend操作。

In [24]: l = [1, 2, 3, 4]                                                       

In [25]: l                                                                      
Out[25]: [1, 2, 3, 4]

In [26]: id(l)                                                                  
Out[26]: 140009508733504

In [27]: l += [5, 6, 7]                                                         

In [28]: l                                                                      
Out[28]: [1, 2, 3, 4, 5, 6, 7]

In [29]: id(l)                                                                  
Out[29]: 140009508733504

使用+创建一个新列表。

In [31]: l                                                                      
Out[31]: [1, 2, 3]

In [32]: id(l)                                                                  
Out[32]: 140009508718080

In [33]: l = l + [4, 5, 6]                                                      

In [34]: l                                                                      
Out[34]: [1, 2, 3, 4, 5, 6]

In [35]: id(l)                                                                  
Out[35]: 140009506500096

现在让我们问您一个问题。

In [36]: t = ([1, 2], [3, 4])                                                   

In [37]: t[0] += [10, 20]                                                       
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-37-5d9a81f4e947> in <module>
----> 1 t[0] += [10, 20]

TypeError: 'tuple' object does not support item assignment

In [38]: t                                                                      
Out[38]: ([1, 2, 10, 20], [3, 4])

+运算符首先在此处执行,这意味着列表将被更新(扩展)。允许这样做是因为列表的引用(存储在元组中的值)不变,所以很好。

然后=尝试更新tuple内部的引用,因为元组是不可变的,因此不允许这样做。

但是实际列表已被+突变。

Python无法更新元组中列表的引用,但由于它会被更新为相同的引用,因此,由于用户看不到更改,因此我们。

因此,+被执行而=无法执行。 +list内已经引用的tuple进行了突变,因此我们在列表中看到了该突变。

答案 2 :(得分:5)

现有答案是正确的,但是我认为文档实际上可以对此提供一些额外的启示:

来自in-place operators documentation

x + = y语句等同于x = operator.iadd(x,y)

所以当我们写

x[0] += [3, 4]

等效于

x[0] = operator.iadd(x[0], [3, 4])
在列表的情况下,

iadd是使用extend实现的,因此我们看到此操作实际上在做两件事:

  • 扩展列表
  • 重新分配索引0

如本文档后面所述:

请注意,当调用就位方法时,计算和分配是在两个单独的步骤中进行的。

第一次操作没问题

第二个操作是不可能的,因为x是一个元组。

但是为什么要重新分配?

在这种情况下,这似乎令人困惑,并且可能想知道为什么+=运算符等效于x = operator.iadd(x, y)而不是简单的operator.iadd(x, y)

这不适用于不可变的类型,例如int和str。因此,虽然iadd对于列表实现为return x.extend(y),但对于整数则实现为return x + y

再次从文档中获得

对于诸如字符串,数字和元组之类的不可变目标,将计算更新后的值,但不会将其分配回输入变量