Python 元组的相对不可变性

一个关于 += 的谜题

Leonardo Rochael 在 2013 年的 Python 巴西会议上提到了这个谜题:

1
2
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]

以上两个表达式到底会发生下面 4 种情况下的哪一种?

  1. t 变成 (1, 2, [30, 40, 50, 60])。
  2. 因为 tuple 不支持对它的元素赋值,所以会抛出 TypeError 异常。
  3. 以上两个都不是。
  4. 1 和 2 都是对的。

刚看到这个问题的时候很多人可能会选择 2,但其实答案是 4。我们用控制台运行这段代码得到下面的结果:

1
2
3
4
5
6
7
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])

变量不是盒子

在回答这个问题之前我们来了解 Python 中的变量。

“变量是盒子”这样的比喻经常被使用,但是这有碍于理解面向对象语言中的引入式变量。如下所示的交互式控制台中,无法使用“变量是盒子”做解释。

1
2
3
4
5
>>> a = [1, 2, 3]
>>> b = a
>>> a.append(4)
>>> b
[1, 2, 3, 4]

如果把变量想象为盒子,那么无法解释 Python 中的赋值。Python 变量类似于 Java 中的引用式变量,因此最好把它们理解为附加在对象上的标注。

为了理解 Python 中的赋值语句,应该始终先读右边(对引用式变量来说,说把变量分配给对象更合理。毕竟,对象在赋值之前就创建了。)。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。

元组不可变性指的是?

元组与多数 Python 集合(列表、字典、集,等等)一样,保存的是对象的引用。如果引用的元素是可变的,及时元组本身不可变,元素依然可变。也就是说,元组的不可变性其实指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。

我们看一下示例:

1
2
3
4
5
6
7
8
>>> t = (1, 2, [30, 40])
>>> id(t[-1])
4389728480
>>> t[-1].append(99)
>>> t
(1, 2, [30, 40, 99])
>>> id(t[-1])
4389728480

查看 t[-1] 列表的标识可以看到标识没变,只是值变了。

回到最初的问题,如果写成如下代码:

1
2
3
4
>>> t = (1, 2, [30, 40])
>>> t[2].extend([50, 60])
>>> t
(1, 2, [30, 40, 50, 60])

我们发现就可以避免异常。

因此,引发异常的操作在于对不可变元组的元素进行了赋值。这也对前面所说的变量是标注作了很好的注释。在分配给变量前对象就已经创建,所以尽管后来的赋值操作引发了异常,列表对象还是发生了变化。

Note

  • 尽量不要把可变对象放在元组里面。
  • 增量赋值(+=)不是一个原子操作。我们刚才看到了,它虽然抛出了异常,但还是完成了操作。