深入理解 python 虚拟机:生成器停止背后的魔法
深入理解 python 虚拟机:生成器停止背后的魔法
在本篇文章当中主要给大家介绍 Python 当中生成器的实现原理,尤其是生成器是如何能够被停止执行,而且还能够被恢复的,这是一个非常让人疑惑的地方。因为这与我们通常使用的函数的直接是相违背的,函数之后执行完成之后才会返回,而生成表面是函数的形式,但是这违背了我们正常的编程直觉。
深入理解生成器与函数的区别
为了从根本上建立对生成器的认识,我们首先就需要深入理解一下生成器和函数的区别。其实在从虚拟机的层面来看,他们两个都是对象,只不过一个是生成器对象,一个是函数对象。在 Python 当中,如果你在函数里面使用了 yield 语句,那么你的这个函数在被调用的时候就不会被执行,而是会返回一个生成器对象。
>>> def bar():
... print("before yield")
... res = yield 1
... print(f"{res = }")
... print("after yield")
... return "Return Value"
...
>>> generator = bar()
>>> generator
<generator object bar at 0x105267510>
>>> bar
<function bar at 0x10562fc40>
>>>
在 Python 当中有的对象是可以直接调用的,比如你自己的类如果实现了__call__方法的话,这个类生成的对象就是一个可调用对象,在 Python 当中一个最常见的可调用对象就是函数了,生成器和函数的区别之一就是,生成器不能够直接被调用,而函数可以。
>>> generator()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not callable
>>>
在上面的代码当中我们要明确 bar 是一个函数,但是这个函数和正常的函数有一点区别,这个函数在被调用的时候不会直接执行代码,而是会返回一个生成器对象,因为在这个函数体当中使用了 yield 语句,我们称这种函数为生成器函数 (generator function),在 Python 当中你可以通过查看一个函数的 co_flags 字段查看一个函数的属性,如果这个字段和 0x0020 进行 & 操作之后的结果大于 0,那么就说明这个函数是一个生成器函数。
>>> (bar.__code__.co_flags & 0x0020) > 0
True
>>> bar.__code__.co_flags & 0x0020
32
从上面的代码当中我们可以看到 bar 就是一个生成器函数,除了上面的方法 Python 的标准库也提供了方法去辅助我们进行判断。
>>> import inspect
>>> inspect.isgeneratorfunction(bar)
True
上面的特性在 Python 程序进行编译的时候,编译器可以做到这一点,当发现一个函数当中存在类似 yield 的语句的时候就在函数的 co_flags 字段当中和 0x0020 进行或操作,然后将这个值保存在 co_flags 当中。
总之生成器和函数之间的关系为:生成器对象是通过调用生成器函数得到的,调用生成器函数的返回对象是生成器。
虚实交错的时空魔法
首先我们需要了解的是,如果我们想让一个生成器对象执行下去的话,我们可以使用 next 或者 send 函数,进行实现:
>>> next(generator)
before yield
1
>>> next(generator)
res = None
after yield
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: Return Value
在 CPython 实现的虚拟机当中,如果我们想要正确的使用 send 函数首先需要让生成器对象执行到第一个 yield 语句,我们可以使用 next(generator) 或者 generator.send(None)。比如在上面的第一条语句当中执行 next(generator),运行到语句 res = yield 1,但是这条语句还没有执行完,需要我们调用 send 函数之后才能够完成赋值操作,send 函数的参数会被赋值给变量 res 。当整个函数体执行完成之后虚拟机就会抛出 StopIteration 异常,并且将返回值保存到 StopIteration 异常对象当中:
>>> generator = bar()
>>> next(generator)
before yield
1
>>> try:
... generator.send("None")
... except StopIteration as e:
... print(f"{e.value = }")
...
res = 'None'
after yield
e.value = 'Return Value'
>>>
上面的代码当中可以看到,我们正确的执行力我们在上面谈到的生成器的使用方法,并且将生成器执行完成之后的返回值保存到异常的 value 当中。
生成器内部实现原理
从上面的关于生成器的使用方式来看,生成器可以在函数执行到一半的时候停止,然后继续恢复执行,为了实现这一点我们就需要有一种手段去保存函数执行的状态。但是我们需要保存函数执行的那些状态呢?最重要的两点就是代码现在执行到什么位置了,因为我们之后要继续从下一条指令开始恢复执行,同时我们需要保存虚拟机的栈空间,就是在执行字节码的时候使用到的 valuestack,注意这不是栈帧,同时还有执行函数的局部变量表,这里主要是保存一些局部变量的。而这些东西都保存在虚拟机的栈帧当中了,这一点我们在前面的文章当中已经详细介绍过了。
因此根据这些分析我们应该知道了,生成器里面最重要的就是一个虚拟机的栈帧数据结构了。一个生成器对象当中一定需要有一个虚拟机的栈帧,在 CPython 的实现当中,生成器对象的数据结构如下:
typedef struct
{
/* The gi_ prefix is intended to remind of generator-iterator. */
PyObject ob_base;
struct _frame *gi_frame;
char gi_running;
PyObject *gi_code;
PyObject *gi_weakreflist;
PyObject *gi_name;
PyObject *gi_qualname;
_PyErr_StackItem gi_exc_state;
} PyGenObject;
- gi_frame: 这个字段就是表示生成器所拥有的栈帧。
- gi_running: 表示协程是否在运行。
- gi_code: 表示对应协程函数的代码(字节码)。
- gi_weakreflist: 用于保存这个栈帧对象保存的弱引用对象。
- gi_name 和 gi_qualname 都是表示生成器的名字,后者更加详细。
- gi_exc_state: 用于保存执行生成器代码之前的程序状态,因为之前的代码可能已经产生一些异常了,这个主要用于保存之前的程序状态,待生成器返回之后就进行恢复。
class A:
def hello(self):
yield 1
if __name__ == '__main__':
g = A().hello()
print(g.__name__)
print(g.__qualname__)
上面的程序输出结果为:
hello
A.hello
生成器对应的字节码行为
我们通过下面的例子来分析一下,生成器 yield 对应的字节码:
>>> import dis
>>> def hello():
... yield 1
... yield 2
...
>>> dis.dis(hello)
2 0 LOAD_CONST 1 (1)
2 YIELD_VALUE
4 POP_TOP
3 6 LOAD_CONST 2 (2)
8 YIELD_VALUE
10 POP_TOP
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
在上面的程序当中只有和生成器相关的字节码为 YIELD_VALUE,在加载完常量 1 之后就会执行 YIELD_VALUE 指令,虚拟机在执行完 yield 指令之后,就会直接返回,此时虚拟机的状态——valuestack 和当前指令执行的位置(在上面的这个例子当中就是 4)都会被保存到虚拟机栈帧当中,当下一次执行生成器的代码的时候就会直接从 POP_TOP 指令直接执行。
我们再来看一下另外一个比较重要的指令 YIELD_FROM:
>>> def generator_b(gen):
... yield from gen
...
>>> dis.dis(generator_b)
2 0 LOAD_FAST 0 (gen)
2 GET_YIELD_FROM_ITER
4 LOAD_CONST 0 (None)
6 YIELD_FROM
8 POP_TOP
10 LOAD_CONST 0 (None)
12 RETURN_VALUE
我们现在用一个简单的例子重新回顾一下程序的行为:
def generator_a():
yield 1
yield 2
def generator_b(gen):
yield from gen
if __name__ == '__main__':
gen = generator_b(generator_a())
print(gen.send(None))
print(gen.send(None))
try:
gen.send(None)
except StopIteration:
print("generator exit")
上面的程序输出结果如下所示:
1
2
generator exit
从上面程序的输出结果我们可以看到 generator_a 的两个值都会被返回,这些魔法隐藏在字节码 YIELD_FROM 当中。YIELD_FROM 字节码会调用栈顶上的生成器对象的 send 方法,并且将参数生成器对象 gen 的返回结果返回,比如 1 和 2 这两个值会被返回到 generator_b ,然后 generator_b 会将这个结果继续传播出来。
- 在这个字节码执行最后会进行判断虚拟机当中是否出现了 StopIteration 异常,如果出现了则说 yield from 的生成器已经执行完了,则 generator_b 继续往下执行。
- 如果没有 StopIteration 异常,则说明 yield from 的生成器没有执行完成,这个时候虚拟机会将当前栈帧的字节码执行位置往前移动,这么做的目的是让下一次生成器执行的时候继续执行 YIELD_FROM 字节码,这就是 YIELD_FROM 能够将另一个生成器对象执行完整的秘密。
总结
在本篇文章当中主要分析的生成器内部实现原理和相关的两个重要的字节码,分析了生成器能够停下来还能够恢复执行的原因。本文最重要的两点就是区分函数和生成器和 YIELD 、YIELD_FROM 两个字节码,生成器是生成器函数返回的对象,YIELD 会直接进行函数返回,虚拟机不会继续往下执行,YIELD_FROM 除了会进行函数返回还会将字节码的执行位置往前移动,以保证 YIELD_FROM 下一次还能够被执行。
本篇文章是深入理解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore
关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。
深入理解 python 虚拟机:生成器停止背后的魔法的更多相关文章
- 深入理解 python 虚拟机:字节码灵魂——Code obejct
深入理解 python 虚拟机:字节码灵魂--Code obejct 在本篇文章当中主要给大家深入介绍在 cpython 当中非常重要的一个数据结构 code object! 在上一篇文章 深入理解 ...
- 深入理解python虚拟机:程序执行的载体——栈帧
深入理解python虚拟机:程序执行的载体--栈帧 栈帧(Stack Frame)是 Python 虚拟机中程序执行的载体之一,也是 Python 中的一种执行上下文.每当 Python 执行一个函数 ...
- 深入理解 Python 虚拟机:元组(tuple)的实现原理及源码剖析
深入理解 Python 虚拟机:元组(tuple)的实现原理及源码剖析 在本篇文章当中主要给大家介绍 cpython 虚拟机当中针对列表的实现,在 Python 中,tuple 是一种非常常用的数据类 ...
- 深入理解 Python 虚拟机:浮点数(float)的实现原理及源码剖析
深入理解 Python 虚拟机:浮点数(float)的实现原理及源码剖析 在本篇文章当中主要分析在 cpython 虚拟机当中 float 类型的实现原理以及与他相关的一些源代码. Float 数据结 ...
- 深入理解 Python 虚拟机:整型(int)的实现原理及源码剖析
深入理解 Python 虚拟机:整型(int)的实现原理及源码剖析 在本篇文章当中主要给大家介绍在 cpython 内部是如何实现整型数据 int 的,主要是分析 int 类型的表示方式,分析 int ...
- 深入理解 Python 虚拟机:复数(complex)的实现原理及源码剖析
深入理解 Python 虚拟机:复数(complex)的实现原理及源码剖析 在本篇文章当中主要给大家介绍在 cpython 虚拟机当中是如何实现 复数 complex 这个数据类型的,这个数据类型在 ...
- 深入理解 Python 虚拟机:集合(set)的实现原理及源码剖析
深入理解 Python 虚拟机:集合(set)的实现原理及源码剖析 在本篇文章当中主要给大家介绍在 cpython 虚拟机当中的集合 set 的实现原理(哈希表)以及对应的源代码分析. 数据结构介绍 ...
- 深入理解 Python 虚拟机:字典(dict)的实现原理及源码剖析
深入理解 Python 虚拟机:字典(dict)的实现原理及源码剖析 在本篇文章当中主要给大家深入介绍一下在 cpython 当中字典的实现原理,在本篇文章当中主要介绍在早期 python3 当中的版 ...
- 深入理解 Python 虚拟机:字节(bytes)的实现原理及源码剖析
深入理解 Python 虚拟机:字节(bytes)的实现原理及源码剖析 在本篇文章当中主要给大家介绍在 cpython 内部,bytes 的实现原理.内存布局以及与 bytes 相关的一个比较重要的优 ...
- 深入理解 Python 虚拟机:字典(dict)的优化
深入理解 Python 虚拟机:字典(dict)的优化 在前面的文章当中我们讨论的是 python3 当中早期的内嵌数据结构字典的实现,在本篇文章当中主要介绍在后续对于字典的内存优化. 字典优化 在前 ...
随机推荐
- k8s实战案例之基于StatefulSet控制器运行MySQL一主多从
1.前言 Pod调度运⾏时,如果应⽤不需要任何稳定的标示.有序的部署.删除和扩展,则应该使⽤⼀组⽆状态副本的控制器来部署应⽤,例如 Deployment 或 ReplicaSet更适合⽆状态服务需求, ...
- CatBoost的分布式训练与调优:解决大规模数据集问题
目录 <CatBoost 的分布式训练与调优:解决大规模数据集问题> 引言 随着深度学习的兴起,大规模数据集的存储和处理成为一个重要的技术挑战.由于数据集的规模巨大,传统的分布式训练方法已 ...
- AR增强现实 之Metaio For Unity 开发 之HelloWorld
开发工具 unity 4.6 MetaioSDK 6.1 代码编辑器 VS 2013 上图不会配置的请自行百度 1.创建项目并且导入Metaio SDk 开发包 2.导入后目录图 ...
- React函数式组件渲染、useEffect顺序总结
参考资料: 深入React的生命周期(上):出生阶段(Mount) 深入React的生命周期(下):更新(Update) 精读<useEffect 完全指南> React组件重新渲染理解 ...
- 简单运维oceanbase
简单运维oceanbase 数据库集群参数修改 直连 proxy 连接 手动修改参数 show parameters like '%xx%' ; alter system set xxx='xx'; ...
- 利用Redis实现向量相似度搜索:解决文本、图像和音频之间的相似度匹配问题
在自然语言处理领域,有一个常见且重要的任务就是文本相似度搜索.文本相似度搜索是指根据用户输入的一段文本,从数据库中找出与之最相似或最相关的一段或多段文本.它可以应用在很多场景中,例如问答系统.推荐系统 ...
- 关于bzoj3306(树)的一些反思
1.加零大法好,用好没烦恼 2.不要瞎开long long 3.万物皆可变成wa 4.如果超时,试图把循环中中的东西拉到外面来
- 关于Linux下服务器MySQL的安装和搭建
一.检测是否已经安装Mysql 检测 # yum list installed | grep mysql //检查安装 # yum -y remove mysql-libs.x86_64 //卸载 / ...
- 如何将训练好的Python模型给JavaScript使用?
前言 从前面的Tensorflow环境搭建到目标检测模型迁移学习,已经完成了一个简答的扑克牌检测器,不管是从图片还是视频都能从画面中识别出有扑克的目标,并标识出扑克点数.但是,我想在想让他放在浏览器上 ...
- spring-mvc系列:详解@RequestMapping注解(value、method、params、header等)
目录 一.@RequestMapping注解的功能 二.@RequestMapping注解的位置 三.@RequestMapping注解的value属性 四.@RequestMapping注解的met ...