惰性求值与生成器
Python 中的惰性求值概念回顾,以及 range 与生成器:用 yield 创建惰性序列,实现无限流式的数据。
惰性求值
抱歉,这里话题跳了一下。你们在第 12 课跳过了这部分,所以我们在这里快速补个入门。
惰性求值(lazy evaluation)就是正规序求值的实现,相对于应用序求值而言。复习一下:第 1 课讨论求值模型时我们说过,Scheme 是应用序语言,即调用过程时所有参数都会先求值。相反,正规序语言把过程参数的求值推迟到真正需要参数值的时候。把过程参数的求值拖到最后一刻(比如拖到某个原始操作非它不可时)才进行,就叫惰性求值。
Python 和 Scheme 类似:定义过程并用参数调用它时,所有参数都会在过程体求值之前求值。
来看这个过程:
def try(a, b):
if a == 0:
return True
else:
return b
求值 try(0, 1/0) 会触发除零错误,因为两个参数都先被求值了。
惰性求值下则不会出错:表达式会返回 True,因为参数 1/0 永远不会被求值——它既没被原始过程用到,也没有被返回。
直接使用时,if 就是惰性的。不信试一下:
$ python
>>> if 0 == 0:
... True
... else:
... 1/0
...
True
看,返回的是 True,因为我们从不需要求值 1/0,所以永远不会报错!这之所以可行,是因为在求值过程中 if 是作为特殊形式(special form)处理的(想想第 11 课的 mc-eval)。要想让每次过程调用都有这种行为,我们就得修改 Python 中 eval 和 apply 的工作方式:过程应用的参数不再在传给 apply 之前立即求值,只在它被返回或被原始使用时才求值。
如果你想更深入地理解解释器中惰性求值的实现,请阅读第 12 课原文。
如果你想在 Python 里摆弄惰性求值包装器的实现,可以参考这个网站上的 lazy.py 模块。
Range 与生成器
我们一直在 for 循环里用 range(),却没怎么想过它的工作原理。range 是一个不可变的惰性序列。幕后,range 创建的序列元素要到需要时才生成。不信?试试 print(range(4)) 和 print([0, 1, 2, 3])。
借助生成器(generator),我们可以创建类似 range() 的序列。在 Python 中,生成器是这样一种函数:它按需计算并 yield(产出)下一个值来生成序列。它们类似于 Scheme 中的流(stream),是惰性序列,与列表这种积极序列(eager sequence,急于把所有元素列出来)相对。
生成器是可迭代的,所以你能在 for 循环里使用它们,就像我们用 range() 一样。你也可以对任何生成器过程调用 next(generator) 来获取后续元素。不过,生成器不能重复迭代——序列用完就没了。如果你对用完的生成器调用 next(),会收到 StopIteration 错误信息。
花点时间想想我们为什么需要生成器。为什么不干脆一直用列表?
yield 是我们把普通函数变成生成器的方式。用它代替 return。来看个例子:
def gen_to(n):
for i in range(n+1):
yield i
现在试试用 for 循环逐个打印元素:
gen_to_7 = gen_to(7)
for i in gen_to(7):
print(i)
现在试试调用 next:
next(gen_to_7)
啊哈!StopIteration 错误!
再来一个无限生成器!试着打印它,然后用 next 一步步取。
def gen_forever():
i = -1
while true:
i += 1
yield i
笔记
作业题 10:成长的烦恼(指数级)
写一个生成器 gen_exp(),接受一个数字 n,从 n 开始,(永远地)生成 n 的 n 的 n……指数塔序列。
例如 gen_exp(2) 的前几个元素应该是 2、(2^2)、((2^2)^2)、(((2^2)^2)^2)。