今天在看书过程中发现了一个问题,还挺有意思的,分享给大家。

下面两个 Python 表达式会产生什么结果?

t = (1, 2, [3, 4])
t[2] += [5, 6]

给四个备选答案:

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

当时看到这个问题,第一反应就是选 2。因为 tuple 是不可变对象,不支持对它的元素赋值,会报错。

但事实上,这道题的正解是 4。

在终端里验证一下:

Python 3.8.2 (default, Oct  2 2020, 10:45:42)
[Clang 12.0.0 (clang-1200.0.32.27)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> t = (1, 2, [3, 4])
>>> t[2] += [5, 6]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

结果是没问题的,​​t​​ 被修改了,但是也报错了。

还可以在 Python Tutor 上分析一下:

网站地址:​pythontutor.com/​

这个网站可以可视化分析 Python 的运行过程和原理。

执行第一个表达式:

一个关于 += 的谜题_不可变对象

执行第二个表达式:

一个关于 += 的谜题_编程语言_02

为什么会这样呢?可以从两个方面来解释:

一、对象类型

Python 中的对象可以分成两类,可变对象和不可变对象,比如一些内置类型:

  1. 可变对象:list,set,dict。
  2. 不可变对象:int,float,bool,string,tuple。

举一个例子:

可变对象:

>>> a = [1, 2, 3]
>>> id(a)
2139167246856
>>> b = a
>>> id(b)
2139167246856
>>> a[1] = 4
>>> a
[1, 4, 3]
>>> b
[1, 4, 3]
>>> id(a)
2139167246856
>>> id(b)
2139167246856

可以看到,改变 ​​a​​​ 的同时 ​​b​​ 也跟着变,因为他们始终指向同一个地址。

不可变对象:

>>> a = (1, 2, 3)
>>> id(a)
2139167074776
>>> b = a
>>> a = (4, 5, 6)
>>> a
(4, 5, 6)
>>> b
(1, 2, 3)
>>> id(a)
2139167075928
>>> id(b)
2139167074776

可以看到,​​a​​​ 的值改变后,它的地址也发生了变化,而 ​​b​​ 还是原来的地址,并且原地址中的内容也没有发生变化。

二、字节码

首先解释一下字节码是什么?

Python 执行程序时会把源码文件编译成字节码文件,存放在 __pycahe 目录内,文件用 ​​.pyc​​​ 结尾。之后如果不再修改源码文件,运行时则直接使用 ​​.pyc​​ 文件编译成机器码,这样不但运行速度快,而且支持多个操作系统。

字节码,其实就是一种中间代码。

下面用 dis 模块来看一下表达式 ​​s[a] += b​​ 的执行过程:

>>> import dis
>>> dis.dis('s[a] += b')
1 0 LOAD_NAME 0 (s)
2 LOAD_NAME 1 (a)
4 DUP_TOP_TWO
6 BINARY_SUBSCR
8 LOAD_NAME 2 (b)
10 INPLACE_ADD
12 ROT_THREE
14 STORE_SUBSCR
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
>>>

通过分析字节码,可以看到其中的关键三步:

  1. ​4 DUP_TOP_TWO​​​:将​​s[a]​​ 存入 TOS(Top Of Stack)。
  2. ​10 INPLACE_ADD​​​:执行​​TOS += b​​​,带入到文章开头的表达式,就相当于向​​t[2]​​​ 中添加元素,因为​​t[2]​​ 是 list,可变对象,所以这一操作没有问题。
  3. ​14 STORE_SUBSCR​​​:将结果保存回​​s[a] = TOS​​​,这相当于将结果重新赋值回​​t​​​,由于​​t​​ 是 tuple,不可变对象,所以报错。

虽然这个问题在平时开发中可能并不常见,但通过分析还是有不少知识点可以深挖的。

简单总结以下三点:

  1. 不要把可变对象放在元组里面。
  2. 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。
  3. 查看 Python 的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助。

以上就是本文的全部内容,如果觉得还不错的话,欢迎点赞转发,多谢