CS61A-hw05_Yield关键词的应用

本文最后更新于:2023年10月31日 上午

CS61A-hw05_Yield关键词的应用

为了理解yield的原理,我们必须首先理解生成器和迭代器的原理。

迭代器(Iterables)

创建一个列表,我们可以依次读取列表中的元素,这种依次读取输出元素的行为称为迭代,被读取的对象称为迭代器。

1
2
3
4
5
6
>>> lst = [1, 2, 3]
>>> for i in lst:
... print(i)
1
2
3

也可以通过列表推导式创建列表(迭代器)。

1
2
3
4
5
6
>>> lst = [x*x for x in range(3)]#········@1
>>> for i in lst:
... print(i)
0
1
4

一切可以通过for...in...进行迭代的对象都可以成为迭代器。迭代器的特征是将所需元素储存起来,以供读取,但当数据量很大时,使用迭代器产生数据则不是一个明智的选择,比如将上述代码@1行中range(3)里的数字改为极其庞大的数字,在@1行代码运行之后,程序便生成一个极占内存的大列表lst,但我们只是想输出一些数,没有必要使一个如此简单的行为消耗那么多资源,由此,我们就需要生成器的帮助。

生成器(Generators)

生成器可以认为是迭代器的一种,不过只能依次迭代使用一次,不能重复迭代。生成器的特点是不储存生成值,即用即产,动态生成。

1
2
3
4
5
6
>>> gen = (x*x for x in range(3))#yield_atom
>>> for i in gen:
... print(i)
0
1
4

可见,生成器推导式与列表推导式的差距只在()[]上,而且生成器对象只能迭代一次(继续迭代会抛出StopIteration错误)。

Yield

yield 表达式的使用类似于return(详细用法参见Official Definition章节),区别在yield会使函数变为生成器函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def create_gen():
... lst = range(3)
... for i in lst:
... yield i*i
...
>>> gen = create_gen() # 创建生成器
>>> print(gen) # gen是一个生成器对象
<generator object create_gen at 0xb7555c34>
>>> for i in gen:
... print(i)
0
1
4

生成器在产生只需要生成一次的输出序列时有很大的优势。迭代(比如for)第一次调用从函数创建的生成器对象时,它将从开头运行函数中的代码,直到到达yield,然后返回循环的第一个值。之后,每个后续调用都将运行在函数中编写的循环的另一次迭代,并返回下一个值,生成器函数体内的代码以yield语句所在处分界,某次循环开始时,函数会从上一次yield停止处开始运行(即运行yield的下一行),直到生成器被判定为空(StopIteration)。

Official Definition[1]

6.2.9. Yield expressions

1
2
yield_atom       ::=  "(" yield_expression ")"#使用"()"包裹生成器推导式,产生生成器
yield_expression ::= "yield" [expression_list | "from" expression]#作为表达式在函数体中使用

yield 表达式在定义 generator 函数或 asynchronous generator 函数时才会用到因此只能在函数定义的内部使用。 在一个函数体内使用 yield 表达式会使这个函数变成一个生成器函数,而在一个 async def 函数的内部使用它则会让这个协程函数变成一个异步生成器函数。 例如:

1
2
3
4
5
def gen():  # defines a generator function
yield 123

async def agen(): # defines an asynchronous generator function
yield 123

由于它们会对外层作用域造成附带影响,yield 表达式不被允许作为用于实现推导式和生成器表达式的隐式定义作用域的一部分。

在 3.8 版更改: 禁止在实现推导式和生成器表达式的隐式嵌套作用域中使用 yield 表达式。

下面是对生成器函数的描述,异步生成器函数会在 异步生成器函数 一节中单独介绍。

当一个生成器函数被调用时,它返回一个名为生成器的迭代器。 然后这个生成器将控制生成器函数的执行。 执行过程会在这个生成器的某个方法被调用时开始。 这时,函数会执行到第一个 yield 表达式,在这里它将再次被挂起,向生成器的调用方返回 expression_list 的值,或者如果 expression_list 被省略则返回 None。 这里所谓的挂起,就是说所有局部状态都会被保留下来,包括局部变量的当前绑定、指令指针、内部求值栈和任何异常处理等等。 当通过调用生成器的某个方法恢复执行时,这个函数的运行就与 yield 表达式只是一个外部调用的情况完全一样。 恢复之后 yield 表达式的值取决于恢复执行所调用的方法。 如果是用 __next__() (通常是通过 fornext() 内置函数) 则结果为 None。 在其他情况下,如果是用 send(),则结果将为传给该方法的值。

所有这些使生成器函数与协程非常相似;它们 yield 多次,它们具有多个入口点,并且它们的执行可以被挂起。唯一的区别是生成器函数不能控制在它在 yield 后交给哪里继续执行;控制权总是转移到生成器的调用者。

try 结构中的任何位置都允许yield表达式。如果生成器在(因为引用计数到零或是因为被垃圾回收)销毁之前没有恢复执行,将调用生成器-迭代器的 close() 方法. close 方法允许任何挂起的 finally 子句执行。

当使用 yield from <expr> 时,所提供的表达式必须是一个可迭代对象。 迭代该可迭代对象所产生的值会被直接传递给当前生成器方法的调用者。 任何通过 send() 传入的值以及任何通过 throw() 传入的异常如果有适当的方法则会被传给下层迭代器。 如果不是这种情况,那么 send() 将引发 AttributeErrorTypeError,而 throw() 将立即引发所转入的异常。

当下层迭代器完成时,被引发的 StopIteration 实例的 value 属性会成为 yield 表达式的值。 它可以在引发 StopIteration 时被显式地设置,也可以在子迭代器是一个生成器时自动地设置(通过从子生成器返回一个值)。

在 3.3 版更改: 添加 yield from <expr> 以委托控制流给一个子迭代器。

当yield表达式是赋值语句右侧的唯一表达式时,括号可以省略。

参见

  • PEP 255 - 简单生成器

    在 Python 中加入生成器和 yield 语句的提议。

  • PEP 342 - 通过增强型生成器实现协程

    增强生成器 API 和语法的提议,使其可以被用作简单的协程。

  • PEP 380 - 委托给子生成器的语法

    引入 yield_from 语法的提议,以方便地委托给子生成器。

  • PEP 525 - 异步生成器

    通过给协程函数加入生成器功能对 PEP 492 进行扩展的提议。

Reference


CS61A-hw05_Yield关键词的应用
https://www.0co.dev/CS61A-hw05-Yield/
作者
Konrad Gerrens
发布于
2023年8月1日
许可协议