深入理解 Python 虚拟机:协程初探——不过是生成器而已

深入理解 Python 虚拟机:协程初探——不过是生成器而已

在 Python 3.4 Python 引入了一个非常有用的特性——协程,在后续的 Python 版本当中不断的进行优化和改进,引入了新的 await 和 async 语法。在本篇文章当中我们将详细介绍一下 Python 协程的原理以及虚拟机具体的实现协程的方式。

什么是协程

Coroutines are computer program components that allow execution to be suspended and resumed, generalizing subroutines for cooperative multitasking.

根据 wiki 的描述,协程是一个允许停下来和恢复执行的程序,从文字上来看这与我们的常识或者直觉是相互违背的,因为在大多数情况下我们的函数都是执行完才返回的。其实目前 Python 当中早已有了一个特性能够做到这一点,就是生成器,如果想深入了解一下生成器的实现原理和相关的字节码可以参考这篇文章 深入理解 Python 虚拟机:生成器停止背后的魔法 。

现在在 Python 当中可以使用 async 语法定一个协程函数(当函数使用 async 进行修饰的时候这个函数就是协程函数),当我们调用这个函数的时候会返回一个协程对象,而不是直接调用函数:

>>> async def hello():
...     return 0
... 
>>> hello()
<coroutine object hello at 0x100a04740>

在 inspect 模块当中也有一个方法用于判断一个函数是否是协程函数:

import inspectasync def hello():return 0print(inspect.iscoroutinefunction(hello)) # True

在 Python 当中当你想创建一个协程的话,就直接使用一个 async 关键字定一个函数,调用这个函数就可以得到一个协程对象。

在协程当中可以使用 await 关键字等待其他协程完成,当被等待的协程执行完成之后,就会返回到当前协程继续执行:

import asyncio
import datetime
import timeasync def sleep(t):time.sleep(t)async def hello():print("start a coroutine", datetime.datetime.now())await sleep(3)print("wait for 3s", datetime.datetime.now())if __name__ == '__main__':coroutine = hello()try:coroutine.send(None)except StopIteration:print("coroutine finished")
start a coroutine 2023-10-15 02:21:33.503505
wait for 3s 2023-10-15 02:21:36.503984
coroutine finished

在上面的程序当中,await sleep(3) 确实等待了 3 秒之后才继续执行。

协程的实现

在 Python 当中协程其实就是生成器,只不过在生成器的基础之上稍微包装了一下,比如在写成当中的 await 语句,其实作用和 yield from 对于生成器的作用差不多,稍微有点细微差别。我们用几个例子来详细分析一下协程和生成器之间的关系:

async def hello():return 0if __name__ == '__main__':coroutine = hello()print(coroutine)try:coroutine.send(None)except StopIteration:print("coroutine finished")

上面的代码的输出结果:

<coroutine object hello at 0x1170200c0>
coroutine finished

在上面的代码当中首先调用 hello 之后返回一个协程对象,协程对象和生成器对象一样都有 send 方法,而且作用也一样都是让协程开始执行。和生成器一样当一个生成器执行完成之后会产生 StopIteration 异常,因此需要对异常进行 try catch 处理。和协程还有一个相关的异常为 StopAsyncIteration,这一点我们在之后的文章详细说。

我们再来写一个稍微复杂一点例子:

async def bar():return "bar"async def foo():name = await bar()print(f"{name = }")return "foo"if __name__ == '__main__':coroutine = foo()try:coroutine.send(None)except StopIteration as e:print(f"{e.value = }")

上面的程序的输出结果如下所示:

name = 'bar'
e.value = 'foo'

上面两个协程都正确的执行完了代码,我们现在来看一下协程程序的字节码是怎么样的,上面的 foo 函数对应的字节码如下所示:

  9           0 LOAD_GLOBAL              0 (bar)2 CALL_FUNCTION            04 GET_AWAITABLE6 LOAD_CONST               0 (None)8 YIELD_FROM10 STORE_FAST               0 (name)10          12 LOAD_GLOBAL              1 (print)14 LOAD_CONST               1 ('name = ')16 LOAD_FAST                0 (name)18 FORMAT_VALUE             2 (repr)20 BUILD_STRING             222 CALL_FUNCTION            124 POP_TOP11          26 LOAD_CONST               2 ('foo')28 RETURN_VALUE

在上面的代码当中和 await 语句相关的字节码有两条,分别是 GET_AWAITABLE 和 YIELD_FROM,在函数 foo 当中首先会调用函数 bar 得到一个协程对象,得到的这个协程对象会放到虚拟机的栈顶,然后执行 GET_AWAITABLE 这条字节码来说对于协程来说相当于没执行。他具体的操作为弹出栈顶元素,如果栈顶元素是一个协程对象,则直接将这个协程对象再压回栈顶,如果不是则调用对象的 __await__ 方法,将这个方法的返回值压入栈顶。

然后需要运行的字节码就是 YIELD_FROM,这个字节码和 “yield from” 语句对应的字节码是一样的,这就是为什么说协程就是生成器(准确的来说还是有点不一样,因为协程只是通过生成器的机制来完成,具体的实现需要编译器、虚拟机和标准库协同工作,才能够很好的完成协程程序,而且在虚拟机当中与协程有关的对象有好几个[都是基于生成器])。如果你不了解 YIELD_FROM 的工作原理,可以参考这篇文章:深入理解 Python 虚拟机:生成器停止背后的魔法。

我们在使用生成器的方式来重写上面的程序:

def bar():yield # 这条语句的主要作用是将函数编程生成器return "bar"def foo():name = yield from bar()print(f"{name = }")return "foo"if __name__ == '__main__':generator = foo()try:generator.send(None) # 运行到第一条 yield 语句generator.send(None) # 从 yield 语句运行完成except StopIteration as e:print(f"{e.value = }")

我们再来看一下 foo 函数的字节码:

  7           0 LOAD_GLOBAL              0 (bar)2 CALL_FUNCTION            04 GET_YIELD_FROM_ITER6 LOAD_CONST               0 (None)8 YIELD_FROM10 STORE_FAST               0 (name)8          12 LOAD_GLOBAL              1 (print)14 LOAD_CONST               1 ('name = ')16 LOAD_FAST                0 (name)18 FORMAT_VALUE             2 (repr)20 BUILD_STRING             222 CALL_FUNCTION            124 POP_TOP9          26 LOAD_CONST               2 ('foo')28 RETURN_VALUE

字节码 GET_YIELD_FROM_ITER 就是从一个对象当中获取一个生成器。这个字节码会弹出栈顶对象,如果对象是一个生成器则直接返回,并且将它再压入栈顶,如果不是则调用对象的 __iter__ 方法,将这个返回对象压入栈顶。后续执行 YIELD_FROM 方法,就和前面的协程一样了。

总结

在本篇文章当中简单的介绍了一下协程是什么以及在 CPython 当中协程是通过什么方式实现的,从字节码的角度来看, 生成器和协程本质上使用的字节码是一样的,都是使用 YIELD_FROM 字节码实现的,协程就是在生成器的基础之上实现的。


本篇文章是深入理解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NewStarCTF2023week2-Unserialize?

代码审计&#xff1a; 定义了一个eval类&#xff0c;该类下有一个私有变量cmd和公有成员函数destruct()&#xff0c;该函数在对象的所有引用都被删除或类被销毁时会自动调用&#xff1b; 调用该函数则会执行一个正则表达式进行正则匹配&#xff0c;过滤掉了一些常用命令和bas…

周记之学习总结

你在人群中看到的每一个耀眼的女孩&#xff0c;都是踩着刀尖过来的。你如履平地般地舒适坦然&#xff0c;当然不配拥有任何光芒&#xff1b; 10.11-10.12 思来想去还是不舍得&#xff0c;搞了一下这个jwt&#xff0c;看了很多视频和博客&#xff0c;一直没看懂&#xff0c;两…

pytorch 模型部署之Libtorch

Python端生成pt模型文件 net.load(model_path) net.eval() net.to("cuda")example_input torch.rand(1, 3, 240, 320).to("cuda") traced_model torch.jit.trace(net, example_input) traced_model.save("model.pt")output traced_model(exa…

没有前端如何测试后端跨域问题

一、问题 前段时间对项目中的跨域做了相关的处理&#xff0c;网上有很多跨域的解决方案。前端解决&#xff0c;后端解决&#xff0c;nginx代理解决。我采用的是在后端中使用Cors来解决跨域的问题。但是前端项目还没有搭建起来&#xff0c;并不知道Cors的解决方案是否会生效&am…

深度学习-AlexNet论文精读

论文相对于博文是第一手资料&#xff0c;并且如果研究的东西比较前沿是搜不到相关的博文。谷歌学术论文名&#xff1a;ImageNet Classification with Deep Convolutional Neural Networks。论文下载论文阅读 一篇论文读三遍&#xff0c;不一定需要读完。第一遍读题目&#xff0…

Potato靶机

信息搜集 设备发现 扫描端口 综合扫描 开放了80端口的HTTP服务和7120端口的SSH服务 目录扫描 扫描目录 看看这个info.php&#xff0c;发现只有php的版本信息&#xff0c;没有可以利用的注入点 SSH突破 hydra 爆破 考虑到 7120 端口是 ssh 服务&#xff0c;尝试利用 hydra …

vue3+vite项目中使用svgIcon

如何在vue2webpack项目中使用svgIcon&#xff1f;参考&#xff1a;手摸手&#xff0c;带你优雅的使用 icon - 掘金 这篇文章主要介绍如何在vue3项目中优雅的使用图标 1、通过 vite-plugin-svg-icons 插件封装SvgIcon组件 npm i vite-plugin-svg-icons -D 2、配置 vite.conf…

机器学习在工业机器人领域有哪些应用?

随着人工智能和机器学习的快速发展&#xff0c;工业机器人领域也迎来了新的机遇和挑战。本文综述了机器学习在工业机器人领域的应用&#xff0c;包括机器人视觉、运动控制、路径规划、故障诊断等方面。通过对相关研究和实际应用的分析&#xff0c;总结了机器学习在工业机器人领…

【C语言】结构体、位段、枚举、联合(共用体)

结构体 结构&#xff1a;一些值的集合&#xff0c;这些值称为成员变量。结构体的每个成员可以是不同类型的变量&#xff1b; 结构体声明&#xff1a;struct是结构体关键字&#xff0c;结构体声明不能省略struct&#xff1b; 匿名结构体&#xff1a;只能在声明结构体的时候声…

在pycharm中运行js文件,附加node.js下载步骤

文章目录 一、前言二、node.js安装和配置(如果之前就安装好了可以直接跳过)1、进入官网下载安装包2、在本地安装node.js3、环境配置4、验证是否安装成功5、修改下载位置(默认是在c盘&#xff0c;这个根据个人需求)6、设置默认模块包7、测试一下是否修改成功(要进入管理员模式的…

11 | JpaRepository 如何自定义

EntityManager 介绍 Java Persistence API 规定&#xff0c;操作数据库实体必须要通过 EntityManager 进行&#xff0c;而我们前面看到了所有的 Repository 在 JPA 里面的实现类是 SimpleJpaRepository&#xff0c;它在真正操作实体的时候都是调用 EntityManager 里面的方法。…

云上攻防-云原生篇K8s安全Config泄漏Etcd存储Dashboard鉴权Proxy暴露

文章目录 云原生-K8s安全-etcd未授权访问云原生-K8s安全-Dashboard未授权访问云原生-K8s安全-Configfile鉴权文件泄漏云原生-K8s安全-Kubectl Proxy不安全配置 云原生-K8s安全-etcd未授权访问 攻击2379端口&#xff1a;默认通过证书认证&#xff0c;主要存放节点的数据&#x…

升级包版本之后Reflections反射包在springboot jar环境下扫描不到class排查过程记录

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是「奇点」&#xff0c;江湖人称 singularity。刚工作几年&#xff0c;想和大家一同进步&#x1f91d;&#x1f91d; 一位上进心十足的【Java ToB端大厂…

LeetCode 1 两数之和

题目描述 链接&#xff1a;https://leetcode.cn/problems/two-sum/?envTypefeatured-list&envId2ckc81c?envTypefeatured-list&envId2ckc81c 难度&#xff1a;简单 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 targ…

Linux system函数返回值

1、语法 #include <stdlib.h>int system(const char *command); 2、函数说明 system()会调用fork()产生子进程&#xff0c;由子进程来调用/bin/sh -c command来执行参数command字符串所代表的命令&#xff0c;此命令执行完后随即返回原调用的进程。 command命令执行完成…

SQL RDBMS 概念

SQL RDBMS 概念 RDBMS是关系数据库管理系统(Relational Database Management System)的缩写。 RDBMS是SQL的基础&#xff0c;也是所有现代数据库系统(如MS SQL Server、IBMDB2、Oracle、MySQL和MicrosoftAccess)的基础。 关系数据库管理系统(Relational Database Management Sy…

python树状打印项目路径

学习这个的需求来自于&#xff0c;我想把项目架构告诉gpt问问它&#xff0c;然后不太会打印项目架构&#x1f602; 联想到Linux的tree指令 import osclass DirectoryTree:def __init__(self, path):self.path pathdef print_tree(self, methoddefault):if method default:sel…

卡顿分析与布局优化

卡顿分析与布局优化 大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。Android系统每隔大概16.6ms发出VSYNC信 号&#xff0c;触发对UI进行渲染&#xff0c;如果每次渲染都成功&#xff0c;这样就能够达到流畅的画面所需要的60fps&#xff0c;为了能够实现60fp…

LabVIEW生产者消费者架构

LabVIEW生产者消费者架构 生产者/消费者模式可以轻松地同时处理多个进程&#xff0c;同时还能以不同速率迭代。 缓冲通信 当多个进程以不同速度运行时&#xff0c;就适合采用进程间缓冲通信。有了足够大的缓冲区后&#xff0c;生产者循环可以以快于消费者循环的速度运行&…

c语言练习89:链表的使用

链表的使用 虽然有这么多的链表的结构&#xff0c;但是我们实际中最常⽤还是两种结构&#xff1a; 单链表 和 双向带头循环链表 1. ⽆头单向⾮循环链表&#xff1a;结构简单&#xff0c;⼀般不会单独⽤来存数据。实际中更多是作为其他数据结 构的⼦结构&#xff0c;如哈希桶、…