深入理解 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 1if __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_VALUE4 POP_TOP3           6 LOAD_CONST               2 (2)8 YIELD_VALUE10 POP_TOP12 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_ITER4 LOAD_CONST               0 (None)6 YIELD_FROM8 POP_TOP10 LOAD_CONST               0 (None)12 RETURN_VALUE

我们现在用一个简单的例子重新回顾一下程序的行为:

def generator_a():yield 1yield 2def generator_b(gen):yield from genif __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、计算机系统基础、算法与数据结构)知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/148142.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

kubernetes集群kubeadm方式安装

测试网络和DNS解析 kubectl run busybox --image busybox:1.28 --restart=Never --rm -it busybox -- sh ping www.baidu,com nslookup kubernetes.default.svc.cluster.localbusybox要用指定的1.28版本,不能用最新版本,最新版本,nslookup会解析不到dns和ip 延长kubernetes…

linux命令行配置音频设备

linux命令行配置音频设备 TLTR在linux命令行播放音乐cmus需要开始声音条件功能才能调节播放的音量&#xff0c;看这个链接&#xff0c;继续折腾&#xff0c;have fun! TLTR 以archLinux为例&#xff0c;把下面软件都装一遍。 sudo pacman -S alsa-utils sudo pacman -S alsa-…

数据结构—栈、队列、链表

一、栈 Stack&#xff08;存取O(1)&#xff09; 先进后出&#xff0c;进去123&#xff0c;出来321。 基于数组&#xff1a;最后一位为栈尾&#xff0c;用于取操作。 基于链表&#xff1a;第一位为栈尾&#xff0c;用于取操作。 1.1、数组栈 /*** 基于数组实现的顺序栈&#…

(枚举 + 树上倍增)Codeforces Round 900 (Div. 3) G

Problem - G - Codeforces 题意&#xff1a; 思路&#xff1a; 首先&#xff0c;目标值和结点权值是直接联系的&#xff0c;最值不可能直接贪心&#xff0c;一定是考虑去枚举一些东西&#xff0c;依靠这种枚举可以遍历所有的有效情况&#xff0c;思考的方向一定是枚举 如果去…

协议-SSL协议-基础概念01-SSL位置-协议套件-握手和加密过程-对比ipsec

SSL的位置-思维导图 参考来源&#xff1a; 华为培训ppt:HCSCE122_SSL VPN技术 ##SSL的位置 SSL协议套件 ​​​​握手阶段&#xff0c;完成验证&#xff0c;协商出密码套件&#xff0c;进而生成对称密钥&#xff0c;用于后续的加密通信。 加密通信阶段&#xff0c;数据由对…

93、Redis 之 使用连接池管理Redis6.0以上的连接 及 消息的订阅与发布

★ 使用连接池管理Redis连接 从Redis 6.0开始&#xff0c;Redis可支持使用多线程来接收、处理客户端命令&#xff0c;因此应用程序可使用连接池来管理Redis连接。 上一章讲的是创建单个连接来操作redis数据库&#xff0c;这次使用连接池来操作redis数据库 Lettuce连接池 支持…

【maven】idea中基于maven-webapp骨架创建的web.xml问题

IDEA中基于maven-webapp骨架创建的web工程&#xff0c;默认的web.xml是这样的。 <!DOCTYPE web-app PUBLIC"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN""http://java.sun.com/dtd/web-app_2_3.dtd" ><web-app><display-name…

ADB的概念、使用场景、工作原理

文章目录 一、adb概念&#xff1a;Android Debug Bridge&#xff0c;一个可以控制安卓设备的通用命令行工具二、adb的使用场景&#xff1a;操作手机设备、app 自动化测试1.传输文件2.兼容性测试&#xff08;手机墙&#xff09;3.云测平台4.测试框架底层封装&#xff1a;APP自动…

Qt扩展-QCustomPlot绘图基础概述

QCustomPlot绘图基础概述 一、概述二、改变外观1. Graph 类型2. Axis 坐标轴3. 网格 三、案例1. 简单布局两个图2. 绘图与多个轴和更先进的样式3. 绘制日期和时间数据 四、其他Graph&#xff1a;曲线&#xff0c;条形图&#xff0c;统计框图&#xff0c;… 一、概述 本教程使用…

[C++ 网络协议] 重叠I/O模型

目录 1. 什么是重叠I/O模型 2. 重叠I/O模型的实现 2.1 创建重叠非阻塞I/O模式的套接字 2.2 执行重叠I/O的Send函数 2.3 执行重叠I/O的Recv函数 2.4 获取执行I/O重叠的函数的执行结果 2.5 重叠I/O的I/O完成确认 2.5.1 使用事件对象&#xff08;使用重叠I/O函数的第六个参…

距离矢量路由协议RIP(含Cisco模拟器实验命令配置)

距离矢量路由协议RIP(含Cisco模拟器实验命令配置) 简介 距离矢量路由协议&#xff08;Routing Information Protocol, RIP&#xff09;是一种内部网关协议&#xff0c;它位于应用层&#xff0c;使用520 UDP端口。RIP基于距离矢量算法&#xff08;Bellham-Ford&#xff09;根据…

专业图像处理软件DxO PhotoLab 7 mac中文特点和功能

DxO PhotoLab 7 mac是一款专业的图像处理软件&#xff0c;它为摄影师和摄影爱好者提供了强大而全面的照片处理和编辑功能。 DxO PhotoLab 7 mac软件特点和功能 强大的RAW和JPEG格式处理能力&#xff1a;DxO PhotoLab 7可以处理来自各种相机的RAW格式图像&#xff0c;包括佳能、…

Python3数据科学包系列(三):数据分析实战

Python3中类的高级语法及实战 Python3(基础|高级)语法实战(|多线程|多进程|线程池|进程池技术)|多线程安全问题解决方案 Python3数据科学包系列(一):数据分析实战 Python3数据科学包系列(二):数据分析实战 Python3数据科学包系列(三):数据分析实战 一: 数据分析与挖掘认知…

【大模型和智能问答系统】

大模型和智能问答系统 大模型前的智能问答系统传统管道式架构存在的问题 大模型在任务型问答系统中应用NLU应用DM如何使用大模型NLG应用 大模型前的智能问答系统 大模型统一代指以ChatGPT为代表的&#xff0c;参数量相比以前模型有明显量级变化的生成模型。 智能问答系统&…

初识Java 12-2 流

目录 中间操作 跟踪与调试 对流元素进行排序 移除元素 将函数应用于每个流元素 在应用map()期间组合流 Optional类型 便捷函数 创建Optional Optional对象上的操作 由Optional组成的流 本笔记参考自&#xff1a; 《On Java 中文版》 中间操作 ||| 中间操作&#xf…

Linux使用之xshell、xftp保姆教学(含安装包,详细使用方法,连接失败解决方法)

前言 链接: FTP&#xff0c;SSH服务器介绍 这是我之前写的一篇博客&#xff0c;其中介绍了Ubuntu操作系统的一些常用命令以及服务器介绍&#xff0c;这篇文章就向大家详细介绍如何安装及应用这些服务器&#xff0c;我以xshell、xftp为例。 安装包&#xff0c;使用方法&#xf…

华为云服务器内网vpc对等连接及微服务内网集群搭建处理

最近需要举办一场活动&#xff0c;某个业务访问量上升&#xff0c;有一定并发场景&#xff0c;为了活动能够顺利举行&#xff0c;解决方案就是将业务进行分布式&#xff0c;分布式部署到不同服务器&#xff0c;平摊用户请求&#xff0c;微服务使用的是SpringCloud Alibabanacos…

阿里云OSS图片存储

阿里云对象存储 OSS&#xff08;Object Storage Service&#xff09;是一款海量、安全、低成本、高可靠的云存储服务&#xff0c;提供最高可达 99.995 % 的服务可用性。多种存储类型供选择&#xff0c;全面优化存储成本。 视频介绍 创建bucket 开发文档 上传文件demo &#x…

树的存储结构以及树,二叉树,森林之间的转换

目录 1.双亲表示法 2.孩子链表 3.孩子兄弟表示法 4.树与二叉树的转换 &#xff08;1&#xff09;树转换为二叉树 &#xff08;2&#xff09;二叉树转换成树 5.二叉树与森林的转化 &#xff08;1&#xff09;森林转换为二叉树 以下树为例 1.双亲表示法 双亲表示法定义了…

Ai4science学习、教育和更多

11 学习、教育和更多 人工智能的进步为加速科学发现、推动创新和解决各个领域的复杂问题提供了巨大的希望。然而&#xff0c;要充分利用人工智能为科学研究带来的潜力&#xff0c;我们需要面对教育、人才培养和公众参与方面的新挑战。在本节中&#xff0c;我们首先收集了关于每…