在STL中,迭代器可以剥离算法和具体数据类型之间的耦合,使得库的维护者只需要为特定的迭代器(如前向迭代器,反向迭代器和随机迭代器)等实现算法即可,而不用关心具体的数据结构。在Python中,迭代器更是无处不在。这篇博客简要介绍Python中的迭代器和生成器,它们背后的原理以及如何实现一个自定义的迭代器/生成器,主要参考了教程Iterators & Generators。
迭代器
使用for
循环时,常常遇到迭代器。如下所示,可能是最常用的一种方式。
1 | for i in range(100): |
在Python中,凡是可迭代的对象(Iterable Object),都可以用上面的方式进行迭代循环。例如,当被迭代对象是字符串时,每次得到的是字符串中的单个字符;当被迭代对象是文本文件时,每次得到的是文件中每一行的字符串;当被迭代对象是字典时,每次得到的是字典的key
。
同样,也有很多函数接受的参数为可迭代对象。例如list()
和tuple()
,当传入的参数为刻碟带对象时,返回的是由迭代返回值组成的列表或者元组。例如
1 | list({'x':1, 'y':2}) # => ['x', 'y'] |
为什么list
或者str
这样的可迭代对象能够被迭代呢?或者,自定义的类满足什么条件,就可以用for x in XXX
这种方法来遍历了呢?
在Python中,有内建的函数iter()
和next()
。一般用法时,iter()
方法接受一个可迭代对象,会调用这个对象的__iter__()
方法,返回作用在这个可迭代对象的迭代器。而作为一个迭代器,必须有“迭代器的自我修养”,也就是实现next()
方法(Python3中改为了__next__()
方法)。
如下面的例子,yrange_iter
是yrange
的一个迭代器。yrange
实现了__iter__()
方法,是一个可迭代对象。调用iter(yrange object)
的结果就是返回一个yrange_iter
的对象实例。
1 | # Version 1.0 使用迭代器类 |
而不停地调用迭代器的next()
方法,就能够不断输出迭代序列。如下所示:
1 | In [3]: yiter = iter(yrange(5)) |
其实,上面的代码略显复杂。在代码量很小,不是很在意代码可复用性时,我们完全可以去掉yrange_iter
,直接让yrange.__iter__()
方法返回其自身实例。这样,我们只需要在yrange
类中实现__iter__()
方法和next()
方法即可。如下所示:
1 | # Version2.0 简化版,迭代器是本身 |
然而,上述的代码仍然存在问题,我们无法指定迭代器生成序列的长度,也就是self.n
实际上并没有用到。如果我只想产生0到10以内的序列呢?
我们只需要加入判断条件,当超出序列边界时,抛出Python内建的StopIteration
异常即可。
1 | # Version3.0 加入边界判断,生成有限长度序列 |
Problem 1
Write an iterator class reverse_iter
, that takes a list
and iterates it from the reverse direction.
1 | class reverse_iter(object): |
生成器
生成器是一种方法,他指定了如何生成序列中的元素,生成器内部包含特殊的yield
语句。此外,生成器函数是懒惰求值,只有当调用next()
方法时,生成器才开始顺序执行,直到遇到yield
语句。yield
语句就像return
,但是并未退出,而是打上断电,等待下一次next()
方法的调用,再从上一次的断点处开始执行。我直接贴出教程中的代码示例。
1 | def foo(): |
生成器表达式
生成器表达式和列表相似,将[]
换为()
即可。如下所示:
1 | for i in (x**2 for x in [1,2,3,4]): |
生成器的好处在于惰性求值,这样一来,我们还可以生成无限长的序列。因为生成器本来就是说明了序列的生成方式,而并没有真的生成那个序列。
下面的代码使用生成器得到前10组勾股数。通过在调用take()
方法时修改传入实参n
的大小,该代码可以很方便地转换为求取任意多得勾股数。生成器的重要作用体现在斜边x
的取值为$[0, \infty]$。如果不使用生成器,恐怕就需要写出好几行的循环语句加上break
配合才可以达到相同的效果。
1 | def integer(start, end=None): |
Problem 2
Write a program that takes one or more filenames as arguments and prints all the lines which are longer than 40 characters.
1 | def readfiles(filenames): |
Problem 3
Write a function findfiles
that recursively descends the directory tree for the specified directory and generates paths of all the files in the tree.
注意get_all_file()
方法中递归中生成器的写法,见SO的这个帖子。
1 | import os |
Problem 4
Write a function to compute the number of python files (.py extension) in a specified directory recursively.
1 | def generate_all_py_file(root): |
Problem 5
Write a function to compute the total number of lines of code in all python files in the specified directory recursively.
1 | def generate_all_line(root): |
Problem 6
Write a function to compute the total number of lines of code, ignoring empty and comment lines, in all python files in the specified directory recursively.
1 | def generate_all_no_empty_and_comment_line(root): |
Problem 7
Write a program split.py
, that takes an integer n
and a filename
as command line arguments and splits the file
into multiple small files with each having n
lines.
1 | def get_numbered_line(filename): |
Problem 9
The built-in function enumerate
takes an iteratable
and returns an iterator
over pairs (index, value)
for each value in the source.
Write a function my_enumerate
that works like enumerate
.
1 | def my_enumerate(iterable): |