生成器的使用

在 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. python连接redis、redis字符串操作、hash操作、列表操作、其他通用操作、管道、django中使用redis

    今日内容概要 python连接redis redis字符串操作 redis之hash操作 redis之列表操作 redis其他 通用操作,管道 django中使用redis 内容详细 1.python ...

  2. 增删改查- 万能map- 模糊查询

    1.编写接口 2.编写对应的mapper种的sql语句 3.测试 接口 public interface UserDao { List<User> getUserList(); //根据I ...

  3. 万字长文详解HBase读写性能优化

    一.HBase 读优化 1. HBase客户端优化 和大多数系统一样,客户端作为业务读写的入口,姿势使用不正确通常会导致本业务读延迟较高实际上存在一些使用姿势的推荐用法,这里一般需要关注四个问题: 1 ...

  4. API 工程化分享

    概要 本文是学习B站毛剑老师的<API 工程化分享>的学习笔记,分享了 gRPC 中的 Proto 管理方式,Proto 分仓源码方式,Proto 独立同步方式,Proto git sub ...

  5. 四、针对redis容灾切换导致"脑裂"的情况

    网上参考到别人博客说,redis容灾切换的时候,有几率出现脑裂的情况. 什么是脑裂: sentinel判断master宕机,切换slave为新master的过程中,业务数据还在持续往原master写入 ...

  6. GIT速查手册

    一.GIT 1.1 简单配置 git是版本控制系统,与svn不同的是git是分布式,svn是集中式 配置文件位置 # 配置文件 .git/config 当前仓库的配置文件 ~/.gitconfig 全 ...

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

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

  8. django三板斧与request对象方法与ORM

    目录 django三板斧 HttpResponse() render() redirect() 网页获取静态文件样式 request对象方法 post请求问题 针对get请求和post请求执行不同代码 ...

  9. break、continue、return中选择一个,我们结束掉它

      在平时的开发过程中,经常会用到循环,在写循环的过程中会有很多判断条件及逻辑,你知道如何结束一个循环吗?在java中有break.continue.reture三个关键字都可以结束循环,我们看下他们 ...

  10. Hadoop入门学习笔记(一)

    Week2 学习笔记 Hadoop核心组件 Hadoop HDFS(分布式文件存储系统):解决海量数据存储 Hadoop YARN(集群资源管理和任务调度框架):解决资源任务调度 Hadoop Map ...