为什么改变元组中的列表会引发异常但无论如何都会改变它?

Why does mutating a list in a tuple raise an exception but mutate it anyway?

我不确定我是否完全理解下面的小片段(在 Py v3.6.7 上)中发生的事情。如果有人能向我解释我们如何成功地改变列表,即使 Python.

抛出错误,那就太好了

我知道我们可以改变一个列表并更新它,但是错误是怎么回事?就像我的印象是,如果有错误,那么 x 应该保持不变。

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

第 (1) 行抛出的 Traceback 是

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

我明白错误的含义,但我无法了解它的上下文。

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

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

据我所知,在 Python 允许列表发生变化之后发生异常,然后希望它尝试重新分配它。我认为它在那里爆炸,因为元组是不可变的。

谁能解释一下幕后发生的事情?

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

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

我们可以这样验证:

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], )

尽管存储在“不可变”元组中,但它不会引发错误并且会在适当的位置进行修改。

这里发生了一些事情。

+= 并不总是 + 然后 =.

如果需要,

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

看看这个例子。

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)

同样,列表 class 对 ++= 有单独的实现。

使用+=实际上在后台执行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 无法更新对元组内列表的引用,但由于它会更新为相同的引用,我们作为用户看不到更改。

因此,+ 被执行,而 = 执行失败。 +tuple 中已经引用的 list 进行了变异,因此我们在列表中看到了变异。

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

来自 in-place operators documentation:

the statement x += y is equivalent to x = operator.iadd(x, y)

所以当我们写

x[0] += [3, 4]

相当于

x[0] = operator.iadd(x[0], [3, 4])

iadd 是使用 extend 实现的,在列表的情况下,所以我们看到这个操作实际上做了两件事:

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

如文档后面所述:

note that when an in-place method is called, the computation and assignment are performed in two separate steps.

第一次操作没有问题

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

但为什么要重新分配?

在这种情况下,这似乎令人费解,人们可能想知道为什么 += 运算符等同于 x = operator.iadd(x, y),而不是简单地 operator.iadd(x, y).

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

再次来自文档:

For immutable targets such as strings, numbers, and tuples, the updated value is computed, but not assigned back to the input variable