Python生成器的使用是怎样的,有哪些知识点
Admin 2022-08-13 群英技术资�
� 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 中生成器的原理其实就是要搞清楚下面两个问�
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 版本中就引进了生成器�
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:[email protected]进行举报,并提供相关证据,查实之后,将立刻删除涉嫌侵权内容�
猜你喜欢
这篇文章主要是和大家一起探索python中的时间处理函数,让大家彻底弄懂时间处理。文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一�
下面是Python3实现的旋转数组的3种算法。一、题目给定一个数组,将数组中的元素向右移� k 个位置,其中 k 是非负数。例如:输入: [1,
进程,一个新鲜的字眼,可能有些人并不了解,它是系统某个运行程序的载体,这个程序可以有单个或者多个进程,一般来说,进程是通过系统CPU 内核数来分配并设置的,我们可以来看下系统中的进程
这篇文章主要介绍了浅谈Python基础之列表那些事�,文中有非常详细的代码示例,对正在学习Python基础的小伙伴们有很好地帮�,需要的朋友可以参考下
这篇文章主要介绍了Python运行时修改业务SQL代码,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一�
成为群英会员,开启智能安全云计算之旅
立即注册Copyright © QY Network Company Ltd. All Rights Reserved. 2003-2020 群英 版权所�
增值电信经营许可证 : B1.B2-20140078