在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): |