> * 原视频地址：[James Bennett - A Bit about Bytes: Understanding Python Bytecode - PyCon 2018](https://www.youtube.com/watch?v=cSSpnq362Bk)
> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)
> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO1/understanding-python-bytecode-pycon-2018.md](https://github.com/xitu/gold-miner/blob/master/TODO1/understanding-python-bytecode-pycon-2018.md)
> * 译者：[cdpath](https://github.com/cdpath)
> * 校对者：[HCMY](https://github.com/HCMY)

# James Bennett — 理解 Python 字节码 — PyCon 2018

> 本文为 PyCon 2018 视频之 James Bennett - A Bit about Bytes: Understanding Python Bytecode 的中文字幕，您可以搭配原视频食用。

0:07  欢迎来到字节漫谈

0:11  今天来聊一下 Python 字节码

0:14  标题除了玩文字游戏

0:17  另有深意

0:20  闲话少说

0:22  有请 Django 核心开发者 James Bennett

0:25  开始演讲

0:36  我想先问个有点儿存在主义色彩的问题

0:38  我们为什么要参加 PyCon

0:41  是因为热爱 Python

0:45  没错吧

0:52  为什么热爱 Python？

0:55  因为我们都明白

0:57  读代码的时间比写代码多得多的道理

1:03  所以要尽力让代码更易读

1:05  当然，我们热爱 Python

1:08  是因为 Python 是为一个简单的观点而生的

1:12  代码应该易读

1:19  Python 清晰、易读、易懂

1:24  即便不是程序员

1:25  也可以看一眼 Python 代码

1:28  理解其中的逻辑

1:30  没错吧？

1:36  这就是 Python

1:41  至少从 Python.org 下载的 CPython 是如此

1:50  接下来我要教给大家，它是哪里来的

1:53  又是如何工作的

1:56  理解它有什么用

1:59  最后如何在实践中

2:01  或者理论中应用它

2:05  不过在此之前

2:07  我们要稍微了解一下电脑是如何运行的

2:08  还要知道编程语言是如何运作的

2:12  我喜欢这条推文

2:14  如此美丽又如此真实

2:19  不过我们的确需要理解计算机的工作原理

2:21  计算机内部的 CPU 处理器是个硅片

2:26  上面雕刻着精心布置的电路

2:32  输入特定的电流

2:35  就能得到另一种模式的电流

2:37  而且模式可以预测

2:40  给这些模式起上名字并赋予含义

2:45  我们就可以说这种电流模式代表加法

2:49  电脑的工作原理就是如此

2:51  我们起的这些名字

2:53  叫做 CPU 指令

2:56  有时也被成为机器码

3:00  如果进一步用便于人类理解的形式展示出来

3:01  就是汇编代码

3:05  不过即便是汇编语言也没有那么容易理解

3:08  你们见过汇编代码吗？

3:11  有多少人愿意一直用汇编写代码？

3:13  我们更愿意写源代码

3:22  美妙、清晰、易读、易懂

3:24  但是计算机只接受二进制指令

3:29  要如何在两者之间架设桥梁呢？

3:33  这些年来，人们尝试过好几种办法

3:35  一些语言通过 Grace Murray Hopper 首先发明的编译器

3:41  将源代码直接编译为机器码

3:45  这些语言就是编译语言

3:47  一些语言借助解释器

3:51  直接在运行时把源代码解释为机器码

3:57  这些是解释型语言

3:59  Python 就是解释型语言

4:02  大家也经常谈到 Python 解释器

4:03  不过还有第三种语言

4:06  一些语言编译得到的指令

4:10  并不适用于真实的物理 CPU

4:16  我是说你可以造一个这样的 CPU，但是至少它现在不存在

4:20  这些语言可以为并不存在的 CPU 编译指令而解释器

4:25  就是模拟 CPU 来执行指令的程序

4:28  解释器理解这些指令

4:30  并将这些指令翻译为真实的 CPU 接受的二进制码

4:36  这种中间指令就是字节码

4:39  有很多语言属于此类

4:41  有人用 Java 吗？

4:45  Java 编译的字节码运行在 Java 虚拟机上

4:47  有人用 .Net 吗？

4:48  还有 C#

4:51  C# 编译的字节码运行在 .Net 虚拟机上

4:55  当然还有 Python

4:58  Python 编译的字节码运行在 Python 虚拟机上

5:02  我们仔细看一看它的工作原理

5:04  这是一个计算斐波纳契数的 Python 函数

5:11  写得很好懂

5:13  先是判断是否小于 2，是的话直接返回

5:18  否则就通过循环得到斐波纳契数

5:23  Python 实际上如何执行这个函数呢？

5:25  有人见过拓展名是 pyc 的文件吗？

5:33  如果你用 Python2 的话，就知道 Python 2 会在

5:35  源代码的路径下放一个同名的 pyc 文件

5:40  如果用的是 Python3，pyc 会放在 `__pycache__ ` 路径下

5:47  你也许听说过这些 pyc 是编译后的 Python

5:50  或许听说过 pyc 可以省去再次编译的时间

5:52  这就是 Python 字节码

5:55  pyc 文件中就是编译源代码得到的字节码

5:59  所以当你下一次运行这段代码

6:01  或者下一次导入这个模块时

6:03  Python 不需要从头再编译一遍

6:08  Python 需要的就是这种格式的字节码来执行

6:13  那要如何理解其工作原理呢？

6:15  假如你用 Python 解释器

6:17  输入了获取斐波纳契的函数

6:20  会得到一个函数对象

6:25  这个对象有一个特殊方法，`__code__`

6:27  也就是 Python code 对象

6:32  有人昨天听了 Emily Morehouse 关于语法解析和 AST（抽象语法树）的演讲吗？

6:36  讲得非常不错

6:38  你可以学到一些 code 对象的知识

6:40  以及 Python 如何使用它

6:42  我们今天要看得却是另一个不同的属性

6:45  从另外的角度

6:46  也就是语法解析接下来的事情

6:48  code 对象含有 Python 所需的一切用来执行函数的东西

6:54  它有一些属性，我们可以看看里面有什么

6:56  以及它是怎么运作的

6:59  有一个属性叫 `co_consts`

7:02  它是元组，其元素是函数体中引用的所有的字面量和常量

7:06  可以看到其中有

7:09  数字 2，0，1

7:11  由 0 和 1 组成的元组

7:13  以及 None

7:17  这里的 None 看上去挺奇怪的

7:20  毕竟函数体中就没有写 None

7:22  但是 Python 把 None 放在这里是有原因的

7:28  Python 函数如果没有显式使用 return

7:33  就会返回 None

7:36  所以元组中有 None

7:45  因为 Python 在编译的时候

7:47  无法获知是否有显式的 return 表达式

7:52  实际上根本就不可能知道

7:55  这些就是字面量

7:59  还有一个属性，`co_varnames`

8:01  其元素是局部变量名

8:06  分别是：n, current 和 next

8:12  另一个属性是 `co_names`

8:15  其中的元素是函数体中引用的 `nonlocal` 变量名

8:18  这个函数没有用到 nonlocal 变量

8:20  所以它就是个空元组

8:22  最后来看看最有意思的属性

8:25  `co_code`

8:30  这就是函数的字节码

8:33  它不是字符串，而是 bytes 对象

8:36  因为 Python3 的实现的缘故

8:42  一些字符可以用 ASCII 表示

8:47  这和 Python 展示 bytes 对象的默认方法有关

8:49  但是它不是字符串，也不能把它当作字符串

8:51  它就是一串字节

8:55  如果我们想知道这一长串字节是什么意思

8:57  不妨先从第一个字节开始

9:02  看上去是个管道符号 `|`

9:06  我不知道你们能不能背过 ASCII 表

9:08  反正我是背不过

9:10  所以我其实不知道管道符号 `|` 对应的十进制数字是什么

9:15  不过我可以让 Python 告诉我

9:21  用 Python 求出 `|` 对应的十进制数字是 124

9:24  所以字节码的第一个字节的值是 124

9:26  这仍然没有什么有用的信息

9:30  好在标准库里有个 `dis` 模块

9:36  其中的 `opname` 数组里面有全部的 Python 字节码指令

9:39  其索引值就是字节码的十进制数值

9:46  由此可以查到 124 对应的字节码操作符是 `LOAD_FAST`

9:48  好了，我们知道第一个字节的十进制数字是 124

9:54  含义是 `LOAD_FAST` 指令

9:57  字节码中的第二个字节是 0

10:00  加起来就是 `LOAD_FAST 0`

10:02  不知道你们留意幻灯片的第一页了没有

10:05  其实就是这里的内容

10:08  `LOAD_FAST 0` 也就是 Python 字节码指令

10:12  准确地讲

10:15  这个指令的意思是在变量名元组中查找索引值是 0 的变量名

10:21  也就是局部变量 n

10:26  把它 push 到调用栈的顶端

10:29  我们稍后会介绍调用栈

10:31  不过现在我得告诉你一个捷径

10:35  刚才我给大家演示的读取字节码的方法非常繁琐

10:38  还有个简单的方法

10:41  `import dis` 然后调用 `dis.dis`

10:44  你可传给它任何东西

10:47  比如函数

10:48  或者源代码字符串

10:50  或者任意类型的 Python 对象

10:52  `dis.dis()` 就会将其解开

10:56  打印出易于阅读的字节码

11:00  传入斐波纳契函数得到的结果

11:02  就是幻灯片第一页的内容

11:05  这就是斐波纳契函数的字节码

11:11  有几点值得注意：

11:12  左边这些数字

11:17  2， 3， 4， 5， 6， 7， 8

11:18  对应源码的行号

11:20  也是每个指令块的起点

11:22  你一定注意到了

11:25  每行源码都对应着多行字节码指令

11:30  每个指令旁边都有一个数字

11:32  而这个数字总是偶数

11:34  有人愿意猜猜它为什么是偶数吗？

11:38  这是 Python3.6 的新特征

11:41  这些数字是字节码的偏移量

11:44  如果你仔细看 `__code__.co_codes`

11:46  输入索引值

11:49  比如 6

11:53  就能得到 `POP_JUMP_IF_FALSE`

11:57  之所以用偶数

11:59  是因为 Python3.6 中

12:02  不是所有字节码指令都有参数

12:04  但是 Python3.6 给每个指令都带了参数

12:07  不管本来有没有参数

12:08  这样每个字节码指令都占2个字节

12:10  这样实现起来也更容易

12:16  也有一些指令的参数太大了

12:19  没办法放到一个字节里

12:21  就会分割成多个字节

12:22  但是一定是两个字节的整数倍

12:24  而对于 Python3.5 或者更早的版本

12:28  对于同样的输入

12:29  你得到的字节码可能就有奇数偏移量

12:31  因为 Python 3.5 中不是所有指令都有参数

12:33  还有一点值得注意

12:37  这些向右三角符号

12:40  比如源代码第 4 行，偏移量 12 

12:42  这里的 `LOAD_CONST`

12:44  以及源代码第 5 行，偏移量 22

12:47  这些是「跳转目标」

12:50  Python 通过这种方式告诉你其他指令可能会跳转到这些地方来

12:57  还记得斐波纳契函数中的循环吗？

12:59  最开始是一个判断

13:01  每次运行到循环的起点

13:04  都要跳转回上一个指令

13:08  这些三角箭头就是说这里可能是其他指令的跳转目标

13:12  好了，看过了一些字节码

13:17  我们也知道如何解析原始字节码

13:19  先拿到字节码

13:22  再手动解析这些字节对应的指令

13:24  或者干脆用 `dis.dis` 来解析

13:26  我们实际上谈了一些 Python 的工作原理

13:29  以及Python 如何使用字节码

13:33  CPython 实现的 Python 虚拟机是面向堆栈的

13:35  换句话说就是它的基础数据结构是栈

13:40  如果你以前没有用过栈的话（这里简要介绍一下）

13:43  栈有点儿像列表

13:45  只不过支持两个非常重要的操作

13:48  栈有两端，就叫做顶和底吧

13:49  一个操作是 push

13:52  也就是把值放到栈顶

13:55  另一个操作是 pop

13:57  也就是从栈顶取值，删除，并返回

14:01  每次调用 Python 函数都会把调用帧 push 到调用栈的栈顶

14:07  调用栈记录着每个被调用的函数

14:09  一旦函数返回对应的调用帧就从调用栈 pop 掉

14:17  返回值 push 到调用帧中

14:18  所以如果调用斐波纳契函数

14:21  稍后有详细说明

14:23  就可以拿到返回值

14:24  当执行调用帧中的调用帧时

14:31  还用会用到另外两个栈

14:34  「计算栈」，也叫做「数据栈」

14:40  Python 用它存储所有用到的数据

14:43  Python 函数的多数计算过程皆在此进行

14:46  而大多数指令都用来操作栈顶元素

14:53  另一个用到的栈是「代码块栈」

14:55  用来记录当前活跃的代码块

15:00  代码块就是诸如 try/except, with 块之类的东西

15:04  Python 需要代码块是因为break 和 continue 之类的语句会作用在当前代码块上

15:11  Python 就得知道当前的代码块是什么

15:13  这可以通过维护代码块栈来实现

15:17  所以每次遇到这种结构

15:19  Python 就将其 push 到代码块栈

15:21  结束后再 pop 掉

15:24  我们再来看一下函数具体是如何执行的

15:27  假如我们想求得第 8 个斐波纳契数

15:31  我们要调用 Python 的斐波纳契函数求解

15:35  而这可以转换为三个字节码指令

15:39  `LOAD_GLOBAL`, `LOAD_CONST` 和 `CALL_FUNCTION`

15:42  仔细看

15:44  最开始计算栈是空的

15:46  第一个指令是 `LOAD_GLOBAL`

15:48  载入全局变量名 `fib`，也就是斐波纳契函数

15:54  需要在 `co_names` 元组中的 nonlocal 变量名中查找

16:01  找到函数之后就把函数对象 push 到计算栈栈顶

16:04  接下来是 `LOAD_CONST`

16:06  这里就是取得常量元组的索引为 1 的元素

16:10  还记得吗

16:12  索引为 0 的元素是 None

16:15  所以我们得到的是整数 8

16:17  也就是函数的参数

16:19  将其 push 到栈顶

16:22  接下来是 `CALL_FUNCTION` 指令

16:26  其参数是 1

16:29  当只使用位置参数时，Python 调用函数的方法是

16:34  将函数 push 到栈顶

16:36  再将位置参数继续 push 到栈顶（也就是函数对象的上面）

16:39  然后调用函数时

16:42  pop 所有的位置参数

16:46  所以栈中下一个元素就是函数对象，pop 出这个函数对象

16:48  再将新栈 push 到调用帧或者调用栈中

16:54  在新调用帧中执行斐波纳契函数

16:56  求得返回值 21

17:00  接下来 pop 调用栈，得到调用帧

17:03  返回值就回到了计算栈

17:10  这就是 Python 逐步执行斐波纳契函数的细节

17:14  这里的 `CALL_FUNCTION` 指令只适用于位置参数

17:18  如果是关键字参数

17:20  就要用 `CALL_FUNCTION_KW` 指令

17:26  如果用到生成器，参数拆包

17:30  `*` 操作符或者 `**` 操作符

17:33  就要用到 `CALL_FUNCTION_EX` 指令

17:39  这就是函数的工作原理

17:42  如果你感兴趣

17:45  可以查阅 Python 标准库文档中的 dis 模块

17:47  dis 模块非常好用

17:53  它列举了所有的字节码指令

17:55  还说明了这些指令的功能，指令接受的参数等等任何你想了解的

18:00  有关Python 字节码的技术细节

18:03  这里再讲几个非常有意思的东西

18:07  dis 模块中有一个函数叫 `distb`

18:12  你可曾遇到莫名其妙的异常

18:15  不知道它到底是哪里抛出的

18:18  `dis.distb` 可以帮上忙

18:25  你可以直接在异常发生之后调用它

18:29  或者传入捕获到的 `traceback` 对象

18:33  `distb` 会解析当前调用栈上活跃的调用帧

18:39  打印出执行过的字节码

18:41  还画箭头直接指向抛出异常的指令

18:46  举个例子

18:48  我把一个数字除以 0

18:51  Python 抛出了异常

18:54  `import dis; dis.distb()`

18:57  就可以打印出执行过的字节码

19:00  如果你还想继续深究细节

19:02  请参阅我在幻灯片结尾处给出的参考资料

19:04  你可以看一下用 C 语言写的 Python 解释器

19:07  这就是 2 小时前 GitHub 上的 Python 字节码解释器的 C 源码

19:16  本质上是一个巨大的 switch 表达式

19:19  查找传入的十进制数指令代表的操作是什么

19:27  好，现在我们对字节码有了一些了解

19:31  但是字节码有什么用呢？

19:34  了解字节码有什么好处？

19:40  你们听过或者用过 Forth 语言吗？

19:46  或者新一些的 Factor 语言？

19:52  Forth 和 Factor 都是面向堆栈的编程语言

19:57  Python 虚拟机也是面向堆栈的

19:59  刚才我们讲过

20:01  基本上都是围绕着把一些东西 push 到栈顶

20:03  在栈顶进行一些操作

20:05  最后把结果 pop 回来

20:08  这个过程和我们熟悉的编程方法有些不同

20:11  但是有很多编程语言都是围绕这个理念设计的

20:13  而且理解这种编程思想也挺不错的

20:18  或许没有实际用到它的一天

20:20  但是你可以学习它

20:22  进而拓展你的编程视野

20:27  而且面向堆栈的编程语言或者虚拟机

20:34  通过很少的几个指令

20:37  和有限的栈操作符就可以实现惊人的功能

20:39  真是非常巧妙

20:42  当然了解字节码也是有实际意义的

20:44  大家都喜欢开 C 语言的玩笑

20:49  都把 C 语言当作是半个汇编语言 <注：这里原文没有听清>

20:51  因为你写的、读的 C 代码可以看出来它会传换成什么机器码

20:59  Python 在某种程度上也是如此

21:03  我们可以学习 Python 字节码

21:05  学习如何理解它

21:07  进而了解我们写的 Python 源码会被翻译成什么字节码

21:11  以及 Python 解释器是如何执行源代码的

21:16  这一切会让你富有洞察力

21:19  你还会了解 Python 的工作原理

21:26  以及所有人都想知道的提高 Python 代码的性能的方法

21:30  看一下这两个函数

21:33  它俩的做的是一件事儿

21:35  计算一周有多少秒

21:37  不过有一种写法更快一些

21:40  你能看出来哪个写法更快吗？

21:46  我希望大家可以好好想一想

21:49  为什么一个函数比另一个更快

21:52  以及如何找出这个函数

21:55  方法就是看字节码

21:56  先用 `dis` 模块得到字节码

22:02  这两个函数的字节码有很大的不同

22:04  可以看到第一个函数的字节码把一天的秒数存入变量

22:09  也就是说需要加载常量

22:12  存入变量

22:15  再读出变量中的值

22:17  加载另一个常量，进行乘法

22:19  最后返回结果

22:22  第二个函数的字节码只用了两个常量的乘法

22:24  而 Python 在编译的时候

22:27  发现这是两个常量的乘法

22:32  这个值又不会变化

22:35  `7 * 86400` 的结果怎么着也不会变

22:41  Python 会对此进行优化

22:43  在编译的时候进行乘法

22:45  实际上就直接返回 604800 了

22:49  其他多余的操作都省去了

22:51  这种优化的确很聪明

22:53  Python 在遇到常量操作的时候都会进行这种优化

23:00  Python 所作的优化不只这一种

23:03  你们听说过 spectre 和 meltdown 吗？

23:05  有所了解吗？

23:08  这两个漏洞主要是分支预测导致的

23:11  也就是处理器会尝试推测 if 语句接下来可能的操作

23:18  Python 也会预测字节码操作

23:22  一些字节码运算符总是成对出现

23:24  比如比较操作后面经常跟着跳转指令

23:29  Python 字节码解释器就会进行优化

23:31  试图推测接下来的操作

23:33  从而充分利用 CPU 的分支预测功能以提高执行速度

23:37  所以还挺不错的

23:41  你还可以回答一些经常出现的性能优化问题

23:44  大家总是问

23:46  为什么字面量列表或者字面量字典比调用 `list` 和 `dict` 更快

23:51  好吧，原因如下

23:55  先用 `{}` 来创建一个字面量字典

23:57  只需要两个指令

24:00:00  如果调用 `dict`

24:02:00  需要三个指令

24:04:00  其中一个还是 `CALL_FUNCTION`

24:06:00  这意味着要往调用栈 push 调用帧

24:07:00  执行函数再把结果 pop 回来

24:10:00  再用现实中的代码举个例子

24:12:00  这个例子非常简单

24:15:00  就是算一下前十个完全平方数

24:17:00  这里没有展示完整的字节码

24:20:00  只是 while 循环对应的字节码

24:22:00  由 15 个字节码指令组成

24:25:00  这段代码可以优化

24:28:00  比如把 while 循环换成 for 循环

24:30:00  用 range 来计数

24:34:00  现在循环体的字节码短了很多

24:36:00  只需要 9 个指令

24:39:00  如果写得更符合 Python 哲学的话

24:42:00  比如用列表推导式

24:43:00  对应的字节码会是什么样子呢？

24:47:00  现在整个函数体的字节码只有 9 个指令

24:48:00  但是不要被表象迷惑了

24:54:00  我把这段字节码放在这里是有原因的

24:57:00  注意，虽然只有 9 个指令

25:00:00  却包含了创建函数和调用函数的指令

25:02:00  所以需要把额外的调用帧 push 到调用栈

25:03:00  在那里执行函数体

25:05:00  执行完再 pop 掉，返回

25:08:00  这种操作会耗费更多资源

25:10:00  即使字节码指令更少

25:15:00  因为不是所有的指令都消耗同样多的资源

25:18:00  我们现在讨论的是不同的字节码以及字节码指令的性能差异

25:24:00  大家都想了解，这种微优化技巧

25:28:00  首先我要强调

25:30:00  Python 很慢

25:32:00  如果你为提高 Python 字节码指令的执行速度而绞尽脑汁

25:35:00  那就会只见树木不见森林

25:37:00  Python 比 C 语言慢太多了

25:39:00  根本没必要考虑这种微优化

25:43:00  如果你想写出闪电般迅捷的 Python 代码

25:52:00  先去仔细浏览一遍 Python 标准库

25:56:00  看看内置函数和内置类

25:58:00  了解哪些是 C 语言实现的

26:01:00  哪些是 Python 实现的

26:03:00  因为谈到速度差异时

26:05:00  通过优化字节码指令得到的提升可能就这么点儿

26:10:00  而换成 C 语言实现的版本

26:12:00  性能提升就有这么多，根本没有可比性

26:16:00  即使如此，你可能想要有一些基本的概念

26:18:00  这里简要介绍几个

26:22:00  你如果读过一些 Python 性能优化指南

26:24:00  可能听说过不要在引用循环内的变量

26:27:00  而是先创建别名再在循环中使用这个别名

26:30:00  这就是为什么 (指向幻灯片)

26:32:00  LOAD 指令之间性能存在差异

26:35:00  `LOAD_CONST` 和 `LOAD_FAST` 比较快

26:38:00  而 `LOAD_NAME` 和 `LOAD_GLOBAL` 相对会慢不少

26:40:00  至于为什么

26:43:00  查找 nonlocal 变量会比较复杂

26:47:00  可能需要在多个命名空间中进行搜索

26:52:00  如果你看一下实现解释器的源码

26:56:00  就会知道这些指令的实现非常繁复

26:57:00  另外，循环和代码块比较慢

27:01:00  可以尽量避免使用

27:03:00  它们会用到 `SETUP_LOOP`，`SETUP_WITH`，`SETUP_EXCEPTION` 这些指令

27:10:00  每次进入或退出循环或者代码块

27:13:00  都需要用到多个指令来进入循环

27:18:00  处理好上下文，push 到代码块栈

27:19:00  执行循环体

27:22:00  如果退出循环还得跳出来

27:24:00  最后 pop 结果

27:26:00  还有一些收尾的清理工作

27:27:00  都是非常耗费资源的指令，可以尽力避免

27:30:00  而访问属性，字典检索，列表索引这些操作也需要留意

27:38:00  这里的 `LOAD_ATTR` 和 `BINARY_SUBSCR` 

27:42:00  你经常会听人说

27:44:00  获取字典或列表里的元素

27:45:00  如果要循环遍历

27:47:00  每次都要引用一次

27:49:00  最好提前用局部变量的别名 <注：这里存疑>

27:53:00  因为循环中的每一步都要进行查找 <注：dict 的 lookup 效率非常高，不知道这里是什么意思>

27:55:00  而这种指令更耗资源

28:01:00  在 `dis` 模块的文档中还有很多类似优化技巧

28:05:00  文档介绍了各种指令供你查阅

28:08:00  还有一些资料值得一读

28:10:00  这里推荐三个

28:13:00  首先是本免费在线电子书，《Python 虚拟机内部原理》

28:20:00  当然欢迎给作者打赏

28:21:00  这本书完整介绍了 Python 解释器内部的工作原理

28:28:00  所有的内部机制

28:31:00  各种栈

28:32:00  各种字节指令

28:36:00  其次是 Allison Kaptur 写的 《用 Python 实现 Python 解释器》

28:39:00  她详尽介绍了实现方法

28:40:00  哦，她还有个 PyCon 演讲

28:43:00  她完完整整地介绍了如何用

28:48:00  合理的数据结构

28:50:00  结合各种字节码操作来用 Python 写一个 Python 解释器

28:52:00  最后，可以读一下 CPython 字节码解释器的源码

28:57:00  其中有一部分就是刚才给你们展示过的那个巨大的 switch 表达式

29:00:00  它大概有一千多行

29:02:00  我看的版本有这么长

29:05:00  至少也有几百行

29:08:00  不过不难读懂

29:09:00  是写得非常好的 C 代码

29:11:00  CPython 的 C 源码的风格还是比较易读的

29:18:00  这些都是不错的参考资料

29:21:00  你还可以在 Twitter 上找到我

29:24:00  我可以回答几个问题

29:28:00  你可以在线上关注我

29:32:00  最后感谢大家的聆听

29:36:00  希望你们有所收获

> 如果发现译文存在错误或其他需要改进的地方，欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR，也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。


---

> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。

