学习“Python函数调用”过程中,经常的需要生成一段字节码。每一次生成,我都需要照着上次的笔记,才能敲出简单的几行生成代码,这让我感到有点羞耻。
我要把这个流程,记在脑子里!
字节码生成流程
Python源文件
此前为了测试闭包,创建了一个名为closure.py的Python文件。
# -*- coding: utf-8 -*-
def get_func():
value = "inner"
def inner_func():
print(value)
return inner_func
show_value = get_func()
show_value()
生成字节码的代码
s = open('closure.py').read()
co = compile(s, 'closure.py', 'exec')
import dis
dis.dis(co)
字节码
于closure.py文件所在目录,打开cmd(在Windows下面测试),敲入py -2,依次执行上述生成代码,得到以下结果:
3 0 LOAD_CONST 0 (<code object get_func at 00000000034EA7B0, file "closure.py", line 3>)
3 MAKE_FUNCTION 0
6 STORE_NAME 0 (get_func)
12 9 LOAD_NAME 0 (get_func)
12 CALL_FUNCTION 0
15 STORE_NAME 1 (show_value)
13 18 LOAD_NAME 1 (show_value)
21 CALL_FUNCTION 0
24 POP_TOP
25 LOAD_CONST 1 (None)
28 RETURN_VALUE
生成过程详述
一、读取文件
s = open('closure.py').read()
如果要得到字节码,是需要先有代码的。代码被保存在文件中,读取一下。使用open函数的默认配置,将文件读进内存中,得到一个file对象,file对象被read之后,得到str,str中存储着closure.py中的代码。
当然,代码可以不止是文件,只要是一个保存了代码的str对象,就ok的。这str对象可以来自于:
- Python文件。
- 直接在Python交互式解释器中敲的。
- 网络传输。
二、编译文件
co = compile(s, 'closure.py', 'exec')
内存中有了代码,字节码怎么来呢?调用build-in函数compile,编译一下,编译的结果为code object(或者是AST object,暂不研究)。
compile(source, filename, mode, flags=0, dont_inherit=False, optimize=- 1)
compile函数必须传递3个参数:
- source,第一步中的str(也可以是byte string、AST object)。
- filename,来源文件的文件名。如果没有,哈,随便给一个也可以。
- mode,'exec'、'eval'和'single'三选一,一系列语句用'exec',单一表达式用'eval',单个交互式语句用'single'。
剩下的3个参数(只摘抄一下):
可选参数 flags 和 dont_inherit 控制应当激活哪个编译器选项以及应当允许哪个future 特性。
optimize 实参指定编译器的优化级别;默认值 -1 选择与解释器的 -O 选项相同的优化级别。显式级别为 0 (没有优化;debug 为真)、1 (断言被删除, debug 为假)或 2 (文档字符串也被删除)。
三、反汇编
import dis
dis.dis(co)
dis函数将编译器和解释器使用的字节码(CPython bytecode)作为输入,输出一种可以用来分析阅读的简单格式。也就是上面所看到的字节码。
dis函数的输入种类可以有很多:
a module, a class, a method, a function, a generator, an asynchronous generator, a coroutine, a code object, a string of source code or a byte sequence of raw bytecode. (模块、类、方法、函数、生成器、异步生成器、协程、代码对象、代码字符、字节码。)
寻找嵌套的函数
以上,字节码的生成流程,便牢牢地刻在脑中。但细看上述字节码,get_func和inner_func去哪儿了?
经过一番探索,原来在编译出来的code对象中,有一个co_consts的成员,它是一个保存常量(constants used)的list。通过co_consts一层一层地往下翻,能够找到所有函数的code object:
>>> dir(co)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> co.co_consts
(<code object get_func at 00000000039B8C30, file "closure.py", line 3>, None)
>>> co.co_consts[0].co_consts
(None, 'inner', <code object inner_func at 00000000039B89B0, file "closure.py", line 6>)
>>> dis.dis(co.co_consts[0].co_consts[2])
7 0 LOAD_DEREF 0 (value)
3 PRINT_ITEM
4 PRINT_NEWLINE
5 LOAD_CONST 0 (None)
8 RETURN_VALUE
>>> co.co_consts[0].co_consts[2]
<code object inner_func at 00000000039B89B0, file "closure.py", line 6>
但这样一层一层地找,依然是麻烦的。
有一个简单办法,直接在最新的Python3环境下调用dis.dis,它会把所有的内容都展现出来。
3 0 LOAD_CONST 0 (<code object get_func at 0x000001335C5DF240, file "closure.py", line 3>)
2 LOAD_CONST 1 ('get_func')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (get_func)
12 8 LOAD_NAME 0 (get_func)
10 CALL_FUNCTION 0
12 STORE_NAME 1 (show_value)
13 14 LOAD_NAME 1 (show_value)
16 CALL_FUNCTION 0
18 POP_TOP
20 LOAD_CONST 2 (None)
22 RETURN_VALUE
Disassembly of <code object get_func at 0x000001335C5DF240, file "closure.py", line 3>:
4 0 LOAD_CONST 1 ('inner')
2 STORE_DEREF 0 (value)
6 4 LOAD_CLOSURE 0 (value)
6 BUILD_TUPLE 1
8 LOAD_CONST 2 (<code object inner_func at 0x000001335C5DF190, file "closure.py", line 6>)
10 LOAD_CONST 3 ('get_func.<locals>.inner_func')
12 MAKE_FUNCTION 8 (closure)
14 STORE_FAST 0 (inner_func)
9 16 LOAD_FAST 0 (inner_func)
18 RETURN_VALUE
Disassembly of <code object inner_func at 0x000001335C5DF190, file "closure.py", line 6>:
7 0 LOAD_GLOBAL 0 (print)
2 LOAD_DEREF 0 (value)
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
参考资料
- docs.python.org/3.10/librar…
- docs.python.org/3.10/librar…
- docs.python.org/3.10/librar… dis#module-dis
附
这是一年半之前学习的内容,其实现在又已经忘记了那几条展示字节码的代码。不过确实脑子中有一个印象,如果想要想起来,看看自己的笔记——即本篇——就行了。
本篇的标题叫“Python字节码生成”,或许“生成”有些歧义,应该叫做“查看”?附上原文链接。