1、获取字节码的堆栈(Stack Frame 栈帧)
def foo():
bar()
def bar():
pass
>>> import dis
>>> dis.dis(foo)
2 0 LOAD_GLOBAL 0 (bar)
2 CALL_FUNCTION 0
4 POP_TOP
6 LOAD_CONST 0 (None)
8 RETURN_VALUE
2、字节码与栈帧的一些属性
字节码通常是由 compile() 函数返回的代码对象,表示原始的字节编译可执行代码。它不包含任何上下文,也不会保存任何默认参数的信息。一般而言一行字节码只会保存诸如被引用的变量名,当前的变量名,代码所在的文件名,变量个数等等
而栈帧是字节码形成的堆栈,用于表示一个执行帧,栈帧会保存很多信息,如命名空间的局部/全局字典,当前正在执行的字节码信息,索引指针等等
3、code.co_flags 解释器 flags 属性(字节码属性,见附录)
def foo():
result = yield 111
print(f'result of yield: {result}')
result2 = yield 222
print(f'result of 2nd yield: {result2}')
return 'done'
>>> import dis
>>> dis.dis(foo)
2 0 LOAD_CONST 1 (111)
2 YIELD_VALUE
4 STORE_FAST 0 (result)
3 6 LOAD_GLOBAL 0 (print)
8 LOAD_CONST 2 ('result of yield: ')
10 LOAD_FAST 0 (result)
12 FORMAT_VALUE 0
14 BUILD_STRING 2
16 CALL_FUNCTION 1
18 POP_TOP
4 20 LOAD_CONST 3 (222)
22 YIELD_VALUE
24 STORE_FAST 1 (result2)
5 26 LOAD_GLOBAL 0 (print)
28 LOAD_CONST 4 ('result of 2nd yield: ')
30 LOAD_FAST 1 (result2)
32 FORMAT_VALUE 0
34 BUILD_STRING 2
36 CALL_FUNCTION 1
38 POP_TOP
6 40 LOAD_CONST 5 ('done')
42 RETURN_VALUE
每一个 Python 的方程的对象都有一个 code.co_flags 属性,这是一串预转换为 int 类型的 bin 二进制比特串,用于记录当前方程的种种标识(flags)
生成器的 flags 特征值为 32,即 100000。当编译器在解释一个方程并产生字节码时,如上述的 foo() ,遇到了 yield 关键字后解释器就会将 100000 加至方程的 code.co_flags 标识中,然后通过比特运算获取 flags 中包含的种种信息:
验证方式:将 32 与 code.co_flags 进行位的与运算 &
>>> bool(foo.__code__.co_flags & 32)
True
4、调用一个带有 yield 的函数
当调用一个函数时,Python 会首先检查 code.co_flags 来识别函数的属性(flags)。所以在调用 foo() 函数时 Python 通过查询属性得知它是一个生成器,所以它并不会执行函数,而是返回一个生成器对象
>>> gen = foo()
>>> type(gen)
<class 'generator'>
该生成器对象会封装一个字节码堆栈,并通过属性 gi_code.co_name (字节码属性,见附录)记住原始函数名(reference):
>>> gen.gi_code.co_name
'foo'
带有 yield 的函数有一个非常特别的特点(以函数 foo() 为例):由函数 foo() 生成的所有生成器实例都会拥有相同的代码,但却拥有相互独立的字节码堆栈。并且生成器的栈帧不存在于当前 Python 程序的栈帧中:它们是存在于内存堆上时刻等待被调用
5、生成器字节码的 “last instruction 最终指令” f_lasti 指针(栈帧属性,见附录)
每一个栈帧都拥有一个最终指令指针,指向该栈帧中最后被调用的那个指令。它的初始值是 -1。对于生成器的栈帧而言,意味着这是一个新的,尚未使用的生成器
>>> gen.gi_frame.f_lasti
-1
当我们第一次使用 send 命令时,生成器会开始运作,直到它到达第一个 yield 后停止。此时我们可以获得 send 的返回值 111,因为这个是生成器 gen 传递给 yield 的的值
此时我们再此查看最终指令指针:
>>> gen.send(None) # 在生成器运行之前只能够 send(None)
111 # 得到第一个 yield 的返回值 111
>>> gen.gi_frame.f_lasti
2 # 指针限制指向了第 2 个字节码
>>> len(gen.gi_code.co_code)
44 # 目前总共执行了 44 个字节的 Python 代码
6、生成器的恢复执行
生成器再暂停后可以在任何方程内的任何时间恢复运行:因为生成器的栈帧它不是真正地存在于整个 Python 程序的栈帧中,它是在内存堆中,所以它无需遵守普通方程的堆栈,可以在任何时候被执行
现在可以尝试再次使用 send 命令,现在这次发送的数据成为了第一个 yield 表达式的值,并赋值给变量 result。然后生成器继续执行,直到遇到第二个 yield
>>> gen.send('hello')
result of yield: hello # send 的数据成为了第一个 yield 的表达式的值,并赋值给 result
222 # 得到了第二个 yield 的返回值
可以通过 gi_frame.f_locals 查看局部变量值
>>> gen.gi_frame.f_locals
{'result': 'hello'}
然后我们再次发送 send 命令:
>>> gen.send('goodbye')
result of 2nd yield: goodbye # send 的数据成为了第二个 yield 的表达式的值,并赋值给 result2
Traceback (most recent call last):
File "<input>", line 1, in <module>
StopIteration: done # foo() 函数的返回值
7、附录:
字节码的常见属性:
co_name:函数名
co_argcount:参数个数(包括默认值)
co_nlocals:函数使用的局部变量个数
co_varname:函数使用的局部变量名的元组,包含所有当前局部变量
co_cellvars:包含闭包中所引用的变量名的元组
co_freevars:包含嵌套函数所使用的自由变量名的元组
co_code:表示原始字节码的字符串
co_consts:包含字节码所用字面量的元组
co_names:包含字节码所用名称的元组
co_filename:被编译的代码所在的文件名
co_firstlineno:函数所在行号
co_lnotab:字符串编码字节码相对于行号的偏移
co_stacksize:所需的堆栈大小
co_flags:解释器 flag
栈帧的常见属性:
f_back:之前的栈帧
f_code:当前正在执行的字节码内容
f_locals:局部变量字典
f_globals:全局变量字典
f_builtins:内置名称字典
f_lineno:当前行号
f_lasti:当前索引指针(指针会随着执行的顺序逐个扫描当前栈帧中的字节码),返回当前行号