本文介绍了python 的生成器,构造一些有趣的惰性计算程序,可以作为python函数式的基础。
1. iterator and generator
众所周知,python3里面的range返回是一个对象而不是列表,它的前身是python 2的xrange。python 2里面的range会生成一个列表,当这个列表很大时,会有严重的性能问题:
1 | for x in range(0,100000): |
可以使用iter(range(0,100000))
将range对象转换为可迭代实例(iterator),下面的代码是和上面代码等价的:
1 | lst=iter(range(0,10)) |
显而易见,其实python的for循环其实是个语法糖,首先隐式将list
(可迭代对象)转换为list_iterator
,然后不断调用迭代器的__next__
函数,直到出现StopIteration
为止;下面是一个迭代器的实例(注意内部函数__next__):
1 | class fibonacci: |
使用也很简单,首先生成一个迭代器对象,然后调用next
函数就可以了,我们这里使用enumerate
函数生成组合迭代器返回索引和内部迭代器返回值(类似的api还有zip、reversed):
1 | fib = fibonacci(20) |
根据python doc的描述,迭代器内部是有状态的,每次取一个值都会消耗迭代器的状态。需要注意的是range并不是一个迭代器,因此可以重复使用,其长度并不会因为遍历过而变化。因此并重复使用一个range对象是安全的,而上面的fib对象并不能重复从fib(0)计算。
另一个普遍使用产生迭代器的方法是生成器(generator),即使用yield
返回值;还有少见的生成器表达式也能构造生成器:
1 | def FibonacciIter(n): |
上面的fibonacci generator和迭代器对象(iterable)使用方法一样,都可以使用next取出下个返回值,当生成n
个fibonacci数后,会耗尽并返回。
1 | fib_gen = FibonacciIter(6) |
除了使用for循环取值,还可以使用生成器构造list,或使用map、filter、reduce
等函数进行计算:
1 | # 使用generator构造list |
为了验证生成器是惰性的,我们使用一个筛法生成素数数列:
1 | # 奇数生成器 |
生成器PrimesIter首次返回2(2是特殊素数,其他素数全为奇数),然后在奇数生成器的基础上进行过滤运算(筛法):首先奇数生成器返回3, 这个是素数,yield n
处返回,it看做一个惰性数列(现在是3之后的奇数),使用filter
函数将这个数列里面所有被3整除的奇数全部过滤掉,并返回一个迭代器重新赋值给it,里面包含所有素数。继续下次循环会过滤掉所有5整除的奇数…实际上这个it会被重复包裹成...filter(__not_divisible(5), filter(__not_divisible(3),it)
,while每循环一次,就多加一层filter。
2. itertools and basical usage
无限生成器可以永久运行下去,很容易造成死循环,幸好python库提供了itertools用于处理这些情况, 我们参考python doc编写了一些使用的itertools扩展 api:
1 | from itertools import islice,takewhile |
有了素数生成器,很容易构造一个正整数分解生成器(参考我写的fold/unfold文章):
1 | # decompose N into \Mult{x_i}, x_i is prime |
验证方法是使用list取出生成器的值:
1 | print(list(DecomposeIter(123))) # [3, 41] |
3 container and iterators
除了list,其他所有容器set/dict都能使用for循环遍历,即说明这些容器是可迭代的(iterable),另外字符串、文件、socket等容器也是可以生成迭代器的。除了使用for循环遍历容器/迭代器,我们还可以使用迭代器/生成器构造容器,第2节里面已经出现很多使用list读取生成器内容的用法,当然也可以用结合enumerate生成dict:
1 | iter = PrimesIter() |
我们在使用filter/map/reduce等函数将容器进行转换时,实际上可看做是对容器的迭代器进行操作(注意他们并不直接返回结果,而是返回另一个迭代器!),比较接近于FP风格。