这是Python解释器简介的第三部分。第一部分请点击这里。第二部分请点击这里。如果你喜欢这个系列的话,那就把它分享到Hacker School吧。我是那里的管理员。
在上一章结尾,我们看到了一些奇怪的字符输出:
12 | >>> foo.func_code.co_code\’d\\x01\\x00}\\x01\\x00|\\x01\\x00|\\x00\\x00\\x17S\’ |
这就是python的字节码。
看完第二部分的讲解后,你应该知道“python字节码”和 “python代码对象”是不相同的。字节码是代码对象众多属性中的一个。我们在代码对象的co_code 属性下找到了字节码。它包含了各种作用于解释器的指令。
那么什么是字节码呢?其实,它就是一系列的字节。这些字节打印出来的样子很奇怪是因为有些字节是能够打印的而有些不能。从分析ord的每个字节中我们看到它们只是数字而已。
12 | >>> [ord(b) for b in foo.func_code.co_code][100, 1, 0, 125, 1, 0, 124, 1, 0, 124, 0, 0, 23, 83] |
这就是那些组成python字节码的字节。解释器会循环接收各个字节,查找每个字节的指令然后执行这个指令。需要注意的是,字节码本身并不包括任何python对象,或引用任何对象。
如果你想知道python字节码的意思,可以去找到CPython解释器文件(ceval.c),然后查阅100的意思、1的意思、0的意思,等等。在后续内容中,我们会这么做的!但暂时可以用更简单的方法: dis 模块。
反汇编字节码的意思就是接收这一系列的字节,然后打印出我们能够理解的字符。这并不是python的工作; dis 模块只是帮助我们了解python内部工作的中间状态。我不支持在产品代码中使用 dis ——它是面向程序员的,不是电脑。
但是,现在我们需要做的正是让一些字节码变得更通俗易懂,所以 dis 是一个非常理想的工具。我们将使用 dis.dis 函数去分析 foo 函数的代码对象。
12345678910111213 | >>> def foo(a):... x = 3... return x + a...>>> import dis>>> dis.dis(foo.func_code) 2 0 LOAD_CONST 1 (3) 3 STORE_FAST 1 (x) 3 6 LOAD_FAST 1 (x) 9 LOAD_FAST 0 (a) 12 BINARY_ADD 13 RETURN_VALUE |
(你通常会看到这种写法:dis.dis(foo),直接分析它的函数对象。这其实是一种简便写法: dis 真正分析的还是代码对象。如果要传递一个函数,那么只能接收到它的代码对象。)
左边那一列数字是原始源代码的行号。第二列是字节码的偏移量:LOAD_CONST在第0行,STORE_FAST在第3行,以此类推。中间那列是字节的名字。它们是为程序员所准备的——解释器是完全不需要的。
最后两列告诉我们一些关于指令参数(如果有的话)的细节。第四列是参数本身。它表示一个指向代码对象其它属性的索引。在这个例子中,LOAD_CONST的参数指向列表co_consts,STORE_FAST的参数指向co_varnames。dis在第四列所指向的的地方查找常数或者名称, 最后在第五列返回给我们它找到的数据。这很容易就能得到证实了:
1234 | >>> foo.func_code.co_consts[1]3>>> foo.func_code.co_varnames[1]\’x\’ |
这也能解释为什么第二个指令STORE_FAST位于字节码的第三行。如果一个字节码有参数的话,那么它相邻的两个字节的参数和它相同。当然,正确的处理这些数据是解释器的工作。
(你也许认为BINARY_ADD应该有参数。这个问题我们会在后面讲到解释器本身的时候再来讨论。)
大家通常把dis当作是python字节码的反汇编程序。这肯定是正确的(dis模块文件中就是这么说的)但同时dis也有其它“法力”:它利用整个代码对象打印出程序员能理解的输出。中间三列的信息实际上是被编码在字节码里的,而第一列和最后一列则告诉我们其它的数据。总而言之,字节码本身其实很简单:它只是一系列的数字而已;它并不包含任何的名字或常数。
那么dis模块是怎么做到将字节和名字对应,如100和LOAD_CONST,并返回的呢?试想一下你会怎么做。如果你的想法是:“嗯……你可以创建一个列表并将字节名按序排列,”或者, “我觉得你可以创建一个字典。然后将名字作为键,将字节值作为值。”那就恭喜你啦!这正是它的做法。文件opcode.py就定义了你所想的那个列表和字典。它的内容基本都是这样的(def_op插入了列表和字典中的对映关系):
1234 | def_op(\’LOAD_CONST\’, 100) # Index in const listdef_op(\’BUILD_TUPLE\’, 102) # Number of tuple itemsdef_op(\’BUILD_LIST\’, 103) # Number of list itemsdef_op(\’BUILD_SET\’, 104) # Number of set items |
看,它甚至有可爱的注释来解释每个字节参数的意思。
好啦,我们现在知道了python字节码是什么(和它不是什么)还能使用dis来分析它。在第四部分,我们会从另一个例子来了解Python是怎么做到能生成字节码的同时,仍然是动态语言的。