书接上回,为什么需要coroutine?我们采用一个经典而浅显的案例:计算Fibnacci数列。翠花,上代码(C代码):
void get_fib(int n, int* result)
{
int i = 0;
for (i = 0; i < n; i++)
{
if (i < 2)
{
result[i] = i;
continue;
}
result[i] = result[i - 1] + result[i - 2];
}
}
上述代码中,参数n指定生成Fibnacci数字的个数,result为存放结果的buffer。这种写法是常见套路之一。我不必细说你就可以想象出调用处是怎样使用这个函数的。这是传统函数的套路。它有一个比较烦人的地方,就是存储空间的管理。如果数列规模n比较大,你不得不很不情愿地准备一个很大的空间用于存储,还可能一直提心吊胆地担心内存溢出或泄漏。
OK, 现在该轮到generator出场了。翠花,上代码(Python):
函数定义处:
def get_fib():
a, b = 0, 1
while 1:
yield b
a, b = b, a+b
函数调用处:
fib = get_fib()
for num in fib:
print num
注意,在这里你看不到准备存储空间的代码,因为确实没有这个动作。同时,yield登台亮相了。
Python版的这个”虚伪”的函数get_fib,在调用语句“fib = get_fib()”中,它其实并未被执行,而是构造并返回了一个被称为generator的对象,然后它被用在接下来的for循环中,用于枚举和打印fibnacci数(想要多少要多少,直到你主动退出循环)。按照官方的说法,这个generator的本质是一个迭代器(iterator),一个对象,它暗含了一个next()方法,该方法在for循环中被隐式调用。for循环每调用一次next方法,get_fib()函数的代码才被真正执行一次,但执行中遇到yield语句后就挂起,紧随yield其后的表达式b被作为本次返回值返回给调用者,本次运行结束。下一次next()再被调用,再从冻结处继续执行。也就是说,每次运行它都只运行函数体的一部分而不是全部,并且能够自动记忆前次停止的位置以便下一次从那里继续执行。
该怎样来认识generator的本质呢?其实,它就像是一个被动吐泡泡的机器,你碰它一下(调用next()方法),它就吐一个泡泡,不碰则不吐。我想这就是其名称“generator”的来历吧,因为它就负责不断地generate数据给调用者。因此,它不需要事先准备好一大块空间存放一堆泡泡,任意时刻它只产生一个泡泡!!!Python的官方文档说,generator是一个轻量级的coroutine实现。Wiki中的coroutine词条说coroutine是一个有多个入口、可以多次挂起和恢复执行的subroutine。这两个解释,和你在本例中看到的完全一致。按我个人的理解,在应用层面上看,generator是采用了时间换空间的策略,即用多次执行的代价换取了节省大片存储空间的好处。对于Fibnacci这个特别简单的例子,如果在C版本中增加若干局部static变量应该可以近似地模拟出来。
那么,这样的时空互换策略真的值得吗?有普遍应用的意义吗?这也是我怀疑的一点。别跟我一样孤陋寡闻。按照Wiki以及有识者的介绍,generator非常实用且应用非常广泛甚至可用在系统编程级别,例如合作性任务(生产者消费者问题)、迭代器、无穷列表(你没法事先准备足够大的空间)、实现pipe。而且,表面上就有一个显著的优势:你不必等所有数据都准备好才开始处理,有一个就处理一个。在读取特别大的文件时,generator也是非常好用的手法。有一位貌似比较资深的工程师提到,利用generator可以很容易地实现Unix编程中非常典型的pipeline架构,有兴趣深入的话,我建议你看看这里。
后记:
我知道即使我这样仔细的举例说明,第一次接触它的人还是不一定能够真的理解。因为这里边的确有编程理念上的冲击。你不能抱着对传统函数的理解不放,这将成为理解generator和coroutine的最大障碍。不过,不要灰心,一次不行就多来几次,可以在网络上多看些相关文章(我建议你尽量看英文的或limodou的解释),或者来找我聊聊,我可以给你演示一些更生动的例子。
你可能会不屑于花费时间和精力来学些这么一个看似怪诞的东西,但是,我亲身的体会是,这宝贝真的值得你花时间来学习一下,即使你是一个老家伙。学习它将会非常有益于扩展你的技术视野以及编程理念,是一种显著的提高。真的,我不忽悠。等你掌握了定身术和时间机器的时候,你一定得请我吃一顿。