PyCon 2018 : 理解 Python 字节码

1,445 阅读28分钟
原文链接: github.com

本文为 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_CONSTCALL_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 为什么字面量列表或者字面量字典比调用 listdict 更快

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_CONSTLOAD_FAST 比较快

26:38:00 而 LOAD_NAMELOAD_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_LOOPSETUP_WITHSETUP_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_ATTRBINARY_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 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏