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

Adi*_*tya 15 python tuples immutability python-3.x

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

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

x = ([1, 2], )
x[0] += [3,4] # ------ (1)
Run Code Online (Sandbox Code Playgroud)

第 (1) 行抛出的 Traceback 是

> TypeError: 'tuple' object doesn't support item assignment.. 
Run Code Online (Sandbox Code Playgroud)

我明白错误意味着什么,但我无法获得它的上下文。

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

> TypeError: 'tuple' object doesn't support item assignment.. 
Run Code Online (Sandbox Code Playgroud)

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

有人可以解释一下引擎盖下发生了什么吗?

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

ipython-image

Gui*_*ute 9

我的直觉是,该行x[0] += [3, 4]首先修改列表本身,因此[1, 2]变为[1, 2, 3, 4]然后它尝试调整抛出 a 的元组的内容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], )
Run Code Online (Sandbox Code Playgroud)

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

  • @Aaron 提出了一个很好的观点。我在这里的评论的其余部分似乎是错误的:“+=t”是两个操作数上“+”的简写,然后赋值回左侧操作数。解释器对存储在“x[0]”中的可变数组执行“+”串联,并且在赋值步骤尝试改变不可变元组之前不会引发错误。 (3认同)
  • `x[0] = x[0] + [3,4]` 不起作用,所以这似乎与 `+=` 有关 (2认同)
  • 因为 `__iadd__` 改变了列表,所以 `__add__` 创建了一个新列表。 (2认同)

Dip*_*ami 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)
Run Code Online (Sandbox Code Playgroud)

类似地,列表类对+和有单独的实现+=

Using+=实际上是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
Run Code Online (Sandbox Code Playgroud)

使用+创建一个新列表。

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

现在让我们来回答你的问题。

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

+操作被执行先到这里,这意味着该列表被更新(延长)。这是允许的,因为对列表的引用(存储在元组中的值)不会改变,所以这很好。

所述=然后尝试更新内部的参考tuple这是不允许的,因为元组是不可变的。

但实际列表被+.

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

因此,+执行和=失败执行。+改变已经被引用的list内部,tuple所以我们在列表中看到了改变。


njz*_*zk2 5

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

来自就地运营商文档

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

所以当我们写

x[0] += [3, 4]
Run Code Online (Sandbox Code Playgroud)

它相当于

x[0] = operator.iadd(x[0], [3, 4])
Run Code Online (Sandbox Code Playgroud)

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

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

如文档后面所述:

请注意,当调用就地方法时,计算和赋值分两个单独的步骤执行。

第一次操作没有问题

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

但是为什么要重新分配呢?

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

这不适用于不可变类型,例如 int 和 str。因此,尽管iadd作为实施return x.extend(y)的名单,它是实现return x + y了整数。

再次来自文档:

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