【python小技巧5】一种特殊的迭代器——生成器

上一章我们讨论了迭代器。在迭代器里,有一种很常用,那就是生成器(Generator)。
开始前的自测:
目录
生成器是指?
生成器函数
生成器对象
生成器表达式
再讲 yield
一、生成器?
生成器可以指:生成器函数、生成器对象、生成器表达式
统称生成器,容易弄混,后面我们尽量使用全称 ⁽¹⁾
它们的共同点是按需产生数据值,从而避免一次性将大量数据存储在内存中
一句话:生成器函数是一个函数,调用产生一个生成器对象。生成器表达式的值是一个生成器对象。

二、生成器函数
生成器函数,顾名思义,本质上是一个函数。该函数可以像普通函数一样调用,但是返回值是一个生成器对象。
比如,我给你一个函数:
当我调用 f(3) 之后,打印一个 "n: 3" 并且返回一个 4。这很简单,对吧?但如果我将 return 改为 yield:
调用 f(3),并没有输出 "n: 3",返回值是一个 generator object,即生成器对象。
当我调用 next(a) 时才会打印 "n: 3"、返回 4
它的特点有:
函数内有必须 yield 关键字(python 解释器依靠这一条来区分生成器函数和普通函数);可以有 return,如果没有 return,会自动在最末尾 return None。
调用后不会直接执行函数内的代码。
返回的不是函数内的 return 值,而是一个生成器对象。
对生成器调用 next() 函数时会运行函数,直到运行到 yield 关键字就把后面的值作为 next() 函数的返回值返回出去(特例:一个单独的 yield 等同于 yield None),然后函数暂停,记住运行的位置,暂时退出函数。下一次调用 next() 时,从上次的位置继续运行函数。
运行完函数或遇到 return 时,则触发 StopIteration,return 的值就是 StopIteration 异常里的值(value 属性)。

三、生成器对象
一定是迭代器、可迭代
迭代器是本身
有两种获得方式:
调用生成器函数
使用生成器表达式
属性:
gi_code
:生成器运行时关联的 code 对象。gi_frame
:返回生成器所在的帧(frame)对象。gi_running
:返回生成器对象是否正在迭代。布尔值。gi_yieldfrom
:如果该生成器是一个委派子生成器,则返回其对应的子生成器;否则返回 None。(详见下文第五节)方法
close
()
:在生成器函数暂停的位置引发 GeneratorExit。如果之后生成器函数正常退出、关闭或引发 GeneratorExit (由于未捕获该异常) 则关闭并返回其调用者。
如果生成器产生了一个值,关闭会引发 RuntimeError。
如果生成器引发任何其他异常,它会被传播给调用者。
如果生成器已经由于异常或正常退出则 close() 不会做任何事。
send(value)
:恢复执行并向生成器函数“发送”一个值。value 参数将成为当前 yield 表达式的结果。
send() 方法会返回生成器所产生的下一个值,或者如果生成器没有产生下一个值就退出则会引发 StopIteration。
当调用 send() 来启动生成器时(第一次),它必须以 None 作为调用参数,因为这时没有可以接收值的 yield 表达式;否则会引发一个 TypeError("can't send non-None value to a just-started generator")。
throw(type[, value[, traceback]])
:在生成器暂停的位置引发type
类型的异常,并返回该生成器函数所产生的下一个值。如果生成器没有产生下一个值就退出,则将引发
StopIteration
异常。如果生成器函数没有捕获传入的异常,或引发了另一个异常,则该异常会被传播给调用者。
__next__()
: 产生下一个值,与 send(None) 等价。一般不直接调用。

四、生成器表达式
A generator expression is a compact generator notation in parentheses.⁽²⁾
生成器表达式会产生一个新的生成器对象。 其句法与推导式相同,区别在于它是用圆括号而不是用方括号或花括号括起来的。
我的理解:生成器表达式就是遍历一个对象,对遍历出来的值进行判断或处理。
写法:
先写一个小括号 `()`
在里面写上用来填充这个“序列”的“代表元素”,不仅可以是变量本身 `x`,也可以做一些处理,比如 `foo(x)`
后边必须是一个 for 循环遍历一个对象,e.g. `(foo(x) for x in [1, 2, 3, 4, 5])`
再后面,就是可选的部分了。可以是其他的 for 循环,也可以是 if 语句,也可以有多层嵌套,每一个 for 或者 if 语句的嵌套范围直到生成器表达式末尾
再看文章开头那个问题,显然它的值是一个生成器对象。如果转化为列表的话 ⁽³⁾,代码等价于:
不难看出,它的作用是将 [0, 10) 范围内能被 3 整除的数重复三遍的。
开始讲点细节上的东西:
生成器表达式中使用的变量会在迭代下一个时被求值,即与普通生成器相同;但是,最左侧
for
子句内的可迭代对象是会被立即求值的。正是由于这个特性,使它比列表推导式更省内存。圆括号在只附带一个参数的调用中可以省略。e.g.
只能迭代一次,再迭代就 StopIteration 啦!

五、再讲 yield
首先,yield 关键字只能用在函数里。
其次,也是很多人不知道的冷知识,yield 不只是语句,也可以是表达式!这个 yield 表达式的值就是生成器对象被调用 .send() 方法被传入的值。一个单独的 next(gen_obj) 或者 gen_obj.__next__() 等价于 gen_obj.send(None)。记住:使用 yield 表达式记得加括号!
e.g. 函数内 `print(yield x)` 是非法的!得写成 `print((yield x))`,外层括号是 print 函数的调用,内层属于 yield 表达式,这个不像生成器表达式一样可以省略!
yield from? 没错,生成器函数中还可以将“生成”这个活委派给子生成器 (Subgenerator)
当使用 yield from <expr> 时,所提供的表达式必须是一个可迭代对象。 迭代该可迭代对象所产生的值会被直接传递给当前生成器方法的调用者。 任何通过 send() 传入的值以及任何通过 throw() 传入的异常如果有适当的方法则会被传给下层迭代器。 如果不是这种情况,那么 send() 将引发 AttributeError 或 TypeError,而 throw() 将立即引发所转入的异常。
END

注释:
⁽¹⁾ 一般说的生成器通常指生成器对象
⁽²⁾ 摘自 https://docs.python.org/3.8/reference/expressions.html
⁽³⁾ 只是方便理解
参考资料:
https://docs.python.org/3.8/reference/expressions.html
https://docs.python.org/3.8/tutorial/classes.html
https://docs.python.org/3.8/howto/functional.html
https://docs.python.org/3.8/reference/datamodel.html
https://docs.python.org/3.8/reference/simple_stmts.html
https://peps.python.org/pep-0380
部分内容使用 ChatGPT 编写。
以上内容如有错误,欢迎指出!