用字节码彻底征服面试官

老当益壮,宁移白首之心?穷且益坚,不坠青云之志。

—— 唐·王勃·《滕王阁序》

真题讲解

请问 Python 程序是怎么运行的?是编译成机器码后在执行的吗?试着与 C 、C++Java 、Shell 等常见语言比较说明。

不少初学者对 Python 存在误解,以为它是类似 Shell 的解释性脚本语言,其实并不是。虽然执行 Python 程序的 python 命令也被称为 Python 解释器,但它其实包含一个 编译器 和一个 虚拟机

当我们在命令行敲下 python xxxx.py 时,python 命令中的编译器首先登场,将 Python 代码编译成 代码 对象。代码 对象包含 字节码 以及执行字节码所需的 名字 以及 常量

当编译器完成编译动作后,接力棒便传给 虚拟机虚拟机 维护执行上下文,逐行执行 字节码 指令。执行上下文中最核心的 名字空间 ,便是由 虚拟机 维护的。

因此,Python 程序的执行原理其实更像 Java ,可以用两个词来概括—— 虚拟机字节码 。不同的是,Java 编译器 javac 与 虚拟机 java 是分离的,而 Python 将两者整合成一个 python 命令。此外,Java 程序执行前必须先完整编译,而 Python 则允许程序启动后再编译并加载需要执行的模块。

pyc 文件保存什么东西,有什么作用?

Python 程序执行时需要先由 编译器 编译成 代码 对象,然后再交由 虚拟机 来执行。不管程序执行多少次,只要源码没有变化,编译后得到的代码对象就肯定是一样的。因此,Python 将代码对象序列化并保存到 pyc 文件中。当程序再次执行时,Python 直接从 pyc 文件中加载代码对象,省去编译环节。当然了,当 py 源码文件改动后,pyc 文件便失效了,这时 Python 必须重新编译 py 文件。

❓如何查看 Python 程序的字节码?

Python 标准库中的 dis 模块,可以对 代码 对象以及 函数 对象进行反编译,并显示其中的 字节码

例如,对于函数 add ,通过 code 字段取到它的 代码 对象,并调用 dis 进行反编译:

1
2
3
4
5
6
7
8
>>> def add(x, y):
...     return x + y
...
>>> dis.dis(add.__code__)
  2           0 LOAD_FAST                0 (x)
              2 LOAD_FAST                1 (y)
              4 BINARY_ADD
              6 RETURN_VALUE

当然了,直接将 函数 对象传给 dis 也可以:

1
2
3
4
5
>>> dis.dis(add)
  2           0 LOAD_FAST                0 (x)
              2 LOAD_FAST                1 (y)
              4 BINARY_ADD
              6 RETURN_VALUE

Python 中变量交换有两种不同的写法,示例如下。这两种写法有什么区别吗?那种写法更好?

1
2
3
4
5
6
7
# 写法一
a, b = b, a

# 写法二
tmp = a
a = b
b = tmp

这两种写法都能实现变量交换,表面上看第一种写法更加简洁明了,似乎更优。那么,在优雅的外表下是否隐藏着不为人知的性能缺陷呢?想要找打答案,唯一的途径是研究字节码:

1
2
3
4
5
6
# 写法一
  1           0 LOAD_NAME                0 (b)
              2 LOAD_NAME                1 (a)
              4 ROT_TWO
              6 STORE_NAME               1 (a)
              8 STORE_NAME               0 (b)
1
2
3
4
5
6
7
8
9
# 写法二
  1           0 LOAD_NAME                0 (a)
              2 STORE_NAME               1 (tmp)

  2           4 LOAD_NAME                2 (b)
              6 STORE_NAME               0 (a)

  3           8 LOAD_NAME                1 (tmp)
             10 STORE_NAME               2 (b)

从字节码上看,第一种写法需要的指令条目也更少:先将两个变量依次加载到栈,然后一条 ROT_TWO 指令将栈中的两个变量交换,最后再将变量依次写回去。注意到,变量加载的顺序与 = 右边一致,写回顺序与 = 左边一致。

而且,ROT_TWO 指令只是将栈顶两个元素交换位置,执行起来比 LOAD_NAMESTORE_NAME 都要快。

至此,我们可以得到结论了—— 第一种变量交换写法更优 :

  • 代码简洁明了,不拖泥带水;
  • 不需要辅助变量 tmp ,节约内存;
  • ROT_TWO 指令比一个 LOAD_NAME STORE_NAME 指令对更有优势,执行效率更高;

更多面试真题

❓请解释 is== 这两个操作的区别。

❓在 Python 中与 None 比较时,为什么要用 is None 而不是 == None

❓请问以下程序输出什么?为什么?

上述的问题,你是否都能对答如流呢?点击 阅读原文,获取详细题解!

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注:

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: