python--基础知识点--从 Python 字节码与栈帧的层面来理解 yield 的机制

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:当前索引指针(指针会随着执行的顺序逐个扫描当前栈帧中的字节码),返回当前行号

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python中的生成器(generator)是一种特殊的迭代器,可以用来逐步生成序列。与普通函数不同,生成器函数使用yield关键字来返回值,而不是return。当函数执行到yield时,函数会暂停并将yield后面的值返回给调用者,但是函数的状态仍然保留在内存中,可以再次恢复执行。 以下是一些生成器的用法: 1. 生成器表达式 Python中的生成器表达式与列表推导式类似,但是使用圆括号而不是方括号。它可以用来生成一个序列,而不是一次性生成整个序列。 示例: ``` gen = (x * x for x in range(10)) for i in gen: print(i) ``` 2. yield语句 yield语句可以用来定义生成器函数。当函数执行到yield语句时,函数会暂停并将yield后面的值返回给调用者。函数的状态仍然保留在内存中,可以再次恢复执行。 示例: ``` def generator_func(): for i in range(10): yield i * i gen = generator_func() for i in gen: print(i) ``` 3. send方法 send方法可以在生成器函数中向生成器发送一个值,并继续执行生成器函数。这个值可以通过yield语句返回。 示例: ``` def generator_func(): while True: x = yield print(x) gen = generator_func() next(gen) gen.send(10) ``` 4. yield from语句 yield from语句可以用来在生成器函数中调用另一个生成器函数。它可以让代码更加简洁,并且能够处理嵌套生成器的情况。 示例: ``` def child_gen(): for i in range(5): yield i def parent_gen(): yield from child_gen() gen = parent_gen() for i in gen: print(i) ``` 以上是一些生成器的用法,希望对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值