生成器的使用

在 Python 中,如果一个函数定义的内部使用了 yield 关键字,那么在执行函数的时候返回的是一个生成器,而不是常规函数的返回值。

我们先来看一个常规函数的定义,下面的函数 f() 通过 return 语句返回 1,那么 print 打印的就是数字 1。

def f():
return 1
print(f())

如果我们将上面的 return 改成 yield,也就是下面这样

def f():
yield 1
yield 2
g = f()
print(g)
print(next(g))
print(next(g))
print(next(g))

最终的输出如下,调用函数 f() 得到的是一个生成器(generator)对象 g,通过 Python 内置的 next() 函数可以驱动生成器往下执行,每调用一次 next() 函数,生成器就会执行到下一个 yield 语句处,并将 yield 语句中的表达式返回,当没有更多 yield 语句时继续执行 next() 函数会触发 StopIteration 异常。

<generator object f at 0x10c963c50>
1
2
Traceback (most recent call last):
File "<string>", line 8, in <module>
StopIteration

当然更优雅的使用生成器的方式是使用 for 循环,如下所示,会依次打印 1、2,并且不会抛出 StopIteration 异常,因为本质上生成器也是一种迭代器,所以可以用 for 循环遍历。另外,生成器也可以用生成器表达式如 g = (i for i "hello world") 来创建,这不是本文重点,就不详细介绍了。

def f():
yield 1
yield 2
for i in f():
print(i)

生成器的原理

要理解 Python 中生成器的原理其实就是要搞清楚下面两个问题

  • 调用包含 yield 语句的函数为什么同普通函数不一样,返回的是一个生成器对象,而不是普通的返回值
  • next() 函数驱动生成器执行的时候为什么可以在函数体中返回 yield 后面的表达式后暂停,下次调用 next() 的时候可以从暂停处继续执行

这两个问题都跟 Python 程序运行机制有关。Python 代码首先会经过 Python 编译器编译成字节码,然后由 Python 解释器解释执行,机制上跟其他解释型语言一样。Python 编译器和解释器配合,就能完成上面两个问题中的功能,这在编译型语言中很难做到。像 C、Golang 会编译成机器语言,函数调用通过 CALL 指令来完成,被调用的函数中遇到 RET 指令就会返回,释放掉被调用函数的栈帧,无法在中途返回,下次继续执行。

虽然操作系统在线程切换的时候也会中断正在执行的函数,再次切换回来的时候继续执行,但是被中断的函数在切换的时候并没有返回值产生,这点与 Python 生成器是不同的,不要混淆了。

下面我们具体来看一下 Python 是如何解决上面两个问题的(基于 CPython 3.10.4)。

生成器的创建

Python 编译器在编译 Python 代码的时候分为词法分析、语法分析、语义分析和字节码生成这几个阶段,在进行语义分析的时候有一项重要的工作是构建符号表,主要用于确定各个变量的作用域,顺带做了一件跟生成器相关的事,也就是在分析过程中如果遇到了 yield 语句就将当前代码块的符号表标记为是生成器。

相关源码如下

static int
symtable_visit_expr(struct symtable *st, expr_ty e)
{
if (++st->recursion_depth > st->recursion_limit) {
PyErr_SetString(PyExc_RecursionError, "maximum recursion depth exceeded during compilation");
VISIT_QUIT(st, 0);
}
switch (e->kind) {
...
case Yield_kind:
if (!symtable_raise_if_annotation_block(st, "yield expression", e)) {
VISIT_QUIT(st, 0);
}
if (e->v.Yield.value)
VISIT(st, expr, e->v.Yield.value);
st->st_cur->ste_generator = 1; // 如果遇到了 yield 语句,就将 ste_generator 标志位置 1
if (st->st_cur->ste_comprehension) {
return symtable_raise_if_comprehension_block(st, e);
}
break;
...
}
...
}

最后在生成字节码的时候,会根据符号表的属性计算字节码对象的标志位,如果 ste_generator 为 1,就将字节码对象的标志位加上 CO_GENERATOR,相关源码如下

static int compute_code_flags(struct compiler *c)
{
PySTEntryObject *ste = c->u->u_ste;
int flags = 0;
if (ste->ste_type == FunctionBlock) {
flags |= CO_NEWLOCALS | CO_OPTIMIZED;
if (ste->ste_nested)
flags |= CO_NESTED;
if (ste->ste_generator && !ste->ste_coroutine)
flags |= CO_GENERATOR; // 如果符号表中 ste_generator 标志位为 1,就将 code 对象的 flags 加上 CO_GENERATOR
if (!ste->ste_generator && ste->ste_coroutine)
flags |= CO_COROUTINE;
if (ste->ste_generator && ste->ste_coroutine)
flags |= CO_ASYNC_GENERATOR;
if (ste->ste_varargs)
flags |= CO_VARARGS;
if (ste->ste_varkeywords)
flags |= CO_VARKEYWORDS;
}
...
return flags;
}

最终 g = f() 会生成下面的字节码

0 LOAD_NAME                0 (f)
2 CALL_FUNCTION 0
4 STORE_NAME 1 (g)

Python 解释器会执行 CALL_FUNCTION 指令,将函数 f() 的调用返回值赋值给 g。CALL_FUNCTION 指令在执行的时候会先检查对应的字节码对象的 co_flags 标志,如果包含 CO_GENERATOR 标志就返回一个生成器对象。相关源码简化后如下

PyObject *
_PyEval_Vector(PyThreadState *tstate, PyFrameConstructor *con, PyObject *locals, PyObject* const* args, size_t argcount, PyObject *kwnames)
{
PyFrameObject *f = _PyEval_MakeFrameVector(tstate, con, locals, args, argcount, kwnames);
if (f == NULL) {
return NULL;
}
// 如果 code 对象有 CO_GENERATOR 标志位,就直接返回一个生成器对象
if (((PyCodeObject *)con->fc_code)->co_flags & CO_GENERATOR) {
return PyGen_NewWithQualName(f, con->fc_name, con->fc_qualname);
}
...
}

可以看到编译器和解释器的配合,让生成器得以创建。

生成器的运行

Python 解释器用软件的方式模拟了 CPU 执行指令的流程,每个代码块(模块、类、函数)在运行的时候,解释器首先为其创建一个栈帧,主要用于存储代码块运行时所需要的各种变量的值,同时指向调用方的栈帧,使得当前代码块执行结束后能够顺利返回到调用方继续执行。与物理栈帧不同的是,Python 解释器中的栈帧是在进程的堆区创建的,如此一来栈帧就完全是解释器控制的,即使解释器自己的物理栈帧结束了,只要不主动释放,代码块的栈帧依然会存在。

执行字节码的主逻辑在 _PyEval_EvalFrameDefault 函数中,其中有个 for 循环依次取出代码块中的各条指令并执行,next(g) 在执行的时候经过层层的调用最终也会走到这个循环里,其中跟生成器相关的源码简化后如下

PyObject* _Py_HOT_FUNCTION _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)
{
...
for (;;) {
opcode = _Py_OPCODE(*next_instr);
switch (opcode) {
case TARGET(YIELD_VALUE): {
retval = POP(); // 将 yiled 后面的表达式的值赋给返回值 retval if (co->co_flags & CO_ASYNC_GENERATOR) {
PyObject *w = _PyAsyncGenValueWrapperNew(retval);
Py_DECREF(retval);
if (w == NULL) {
retval = NULL;
goto error;
}
retval = w;
}
f->f_state = FRAME_SUSPENDED; // 设置当前栈帧为暂停状态
f->f_stackdepth = (int)(stack_pointer - f->f_valuestack);
goto exiting; // 结束本次函数调用,返回上级函数
}
}
}
...
}

可以看出 Python 解释器在执行 yield 语句时会将 yield 后面的值作为返回值直接返回,同时设置当前栈帧为暂停状态。由于这里的栈帧是保存在进程的堆区的,所以当这次对生成器的调用结束之后,其栈帧依然存在,各个变量的值依然保存着,下次调用的时候可以继续当前的状态往下执行。

总结

本文介绍了 Python 中生成器的使用方法,然后介绍了 Python 代码的运行机制,并结合源码对生成器的工作原理做了介绍。Python 解释器能实现生成器,主要是因为其是用软件来模拟硬件的行为,既然是软件,在实现的时候就可以添加很多功能,对解释器的一顿魔改,在 Python 2.2 版本中就引进了生成器。

Python 中生成器的原理的更多相关文章

  1. Python中生成器和yield语句的用法详解

    Python中生成器和yield语句的用法详解 在开始课程之前,我要求学生们填写一份调查表,这个调查表反映了它们对Python中一些概念的理解情况.一些话题("if/else控制流" ...

  2. Python中生成器和迭代器的区别(代码在Python3.5下测试):

    https://blog.csdn.net/u014745194/article/details/70176117 Python中生成器和迭代器的区别(代码在Python3.5下测试):Num01–& ...

  3. python中生成器对象和return 还有循环的区别

    python中生成器对象和return 还有循环的区别 在python中存在这么一个关键字yield,这个关键字在项目中经常被用到,比如我写一个函数不想它只返回一次就结束那我们就不能用return,因 ...

  4. python中“生成器”、“迭代器”、“闭包”、“装饰器”的深入理解

    python中"生成器"."迭代器"."闭包"."装饰器"的深入理解 一.生成器 1.生成器定义:在python中,一边 ...

  5. Python中的浮点数原理与运算分析

    Python中的浮点数原理与运算分析 本文实例讲述了Python中的浮点数原理与运算.分享给大家供大家参考,具体如下: 先看一个违反直觉的例子:     >>> s = 0. > ...

  6. python中生成器generator

    通过列表生成式,我们可以直接创建一个列表.但是,受到内存限制,列表容量肯定是有限的.而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素 ...

  7. Python中生成器的理解

    1.生成器的定义 在Python中一边循环一边计算的机制,称为生成器 2.为什么要有生成器 列表所有的数据都存在内存中,如果有海量的数据将非常耗内存 如:仅仅需要访问前面几个元素,那后面绝大多数元素占 ...

  8. python中生成器

    1.简介 通过列表生成式,我们可以直接创建一个列表,但是受到内存的限制,列表容量肯定是有限的. 如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢? 在Pytho ...

  9. python中“生成器”、“迭代器”、“闭包”、“装饰器”的深入理解

    一.生成器 1.什么是生成器? 在python中,一边循环一边计算的机制,称为生成器:generator. 2.生成器有什么优点? 1.节约内存.python在使用生成器时对延迟操作提供了支持.所谓延 ...

随机推荐

  1. PicLite 开发日志 (v0.0.3)

    PicLite 开发日志 (v0.0.3) 感谢您阅读本片文章! Gitee 地址:https://gitee.com/XiaoQuQuSD/pic-lite. 新增功能 当错误出现时不再强制 rai ...

  2. ChCore Lab1 机器启动 实验笔记

    本文为上海交大 ipads 研究所陈海波老师等人所著的<现代操作系统:原理与实现>的课程实验(LAB)的学习笔记的第一篇. 书籍官网:现代操作系统:原理与实现,里面有实验的参考指南和代码仓 ...

  3. 五四青年节,今天要学习。汇总5道难度不高但可能遇到的JS手写编程题

    壹 ❀ 引 时间一晃,今天已是五一假期最后一天了,没有出门,没有太多惊喜与意外.今天五四青年节,脑子里突然想起鲁迅先生以及悲欢并不相通的话,我的五一经历了什么呢,忍不住想说那大概是,父母教育孩子大声嚷 ...

  4. JavaScript 数据结构与算法3(链表)

    学习数据结构的 git 代码地址: https://gitee.com/zhangning187/js-data-structure-study 1.链表 本章学习如何实现和使用链表这种动态的数据结构 ...

  5. C#中的接口和类有什么异同

    不同点: 1. 不能直接实例化接口 2. 接口中的成员不能用访问修饰符修饰(默认public) 3. 接口不包含方法的实现 4. 接口可以多继承,类只能单继承. 5. 类定义可在不同的源文件之间进行拆 ...

  6. Caused by: java.lang.Exception: No native library is found for os.name=Mac and os.arch=aarch64. path=/org/sqlite/native/Mac/aarch64

    编译项目报错: Caused by: java.lang.Exception: No native library is found for os.name=Mac and os.arch=aarch ...

  7. [codeforces] 暑期训练之打卡题(二)

    每个标题都做了题目原网址的超链接 Day11<Given Length and Sum of Digits...> 题意: 给定一个数 m 和 一个长度 s,计算最大和最小在 s 长度下, ...

  8. MySql笔记Ⅱ

    MySql笔记2: part3:(table相关的操作) 数据的增删改 create table t1( id int primary key auto_increment, username cha ...

  9. 羿网通WT2100网络测试仪端口开关功能应用案例

    端口开关是羿网通WT2100具备的一项全局性的功能,使用客户端软件Packlark连接WT2100后无需进入具体功能即可使用.该功能是通过控制设备上的以太网开关实现快速.便捷地切换网口通断状态的目标, ...

  10. MySQL锁(乐观锁、悲观锁、多粒度锁)

    锁 并发事务可能出现的情况: 读-读事务并发:此时是没有问题的,读操作不会对记录又任何影响. 写-写事务并发:并发事务相继对相同的记录做出改动,因为写-写并发可能会产生脏写的情况,但是没有一个隔离级别 ...