- 原视频地址:James Bennett - A Bit about Bytes: Understanding Python Bytecode - PyCon 2018
- 译文出自:掘金翻译计划
- 本文永久链接:github.com/xitu/gold-m…
- 译者:cdpath
- 校对者:HCMY
本文为 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 希望你们有所收获
如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、 iOS、 前端、后端、区块链、 产品、 设计、 人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、 官方微博、知乎专栏。