在调用普通函数时,程序会中断调用代码运行,切换到调用函数的第一行代码开始执行,到return结束。并且将控制还给调用者,被调用函数状态结束并清空(局部变量等)。如果再次调用该函数,我们需要一切从头重新来过。生成器(协程)就是一种不同于这种模式的新方式,具有非常强大的功能,可以简化我们的代码逻辑,更可以降低内存消耗,提高代码质量与效率。

0x01 问题提出

来看下面一个例子。我们需要写一个函数,用来提取一个序列中的全部素数。输入参数是待提取的序列,返回提取的素数序列。

#/usr/bin/env python
#coding:utf-8

__author__ = 'kikay'

import math

#判断一个数值是否是素数
def isPrime(iNum):
    if iNum>1:
        if iNum==2:
            return True
        if iNum%2==0:
            return False
        for i in range(3,int(math.sqrt(iNum)+1),2):
            if iNum%i==0:
                return False
        return True
    return False

#列表生成方法
def GetByList(iList):
    res=list()
    for i in iList:
        if isPrime(i):
            res.append(i)
    return res

if __name__=='__main__':
    L=xrange(0,100)
    res=GetByList(L)
    print type(res)
    for i in res:
        print i,

输出结果:

<type 'list'>
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

0x02 生成器

上面的GetByList函数我们可以用生成器进行优化(优化代码以及内存空间),修改后完整代码如下:

#/usr/bin/env python
#coding:utf-8

__author__ = 'kikay'

import math

#判断一个数值是否是素数
def isPrime(iNum):
    if iNum>1:
        if iNum==2:
            return True
        if iNum%2==0:
            return False
        for i in range(3,int(math.sqrt(iNum)+1),2):
            if iNum%i==0:
                return False
        return True
    return False

#列表生成方法
def GetByList(iList):
    res=list()
    for i in iList:
        if isPrime(i):
            res.append(i)
    return res

#生成器
def GetByGenerator(iList):
    return (i for i in iList if isPrime(i))

if __name__=='__main__':
    L=range(0,100)
    #res=GetByList(L)
    res=GetByGenerator(L)
    print type(res)
    for i in res:
        print i,

运行结果:

<type 'generator'>
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

用GetByGenerator代替了函数GetByList,不仅代码更加简单,而且节约了内存。上面的改进体现在把返回的结果从List列表对象改成了Generator生成器对象,但是问题来了,我们需要提取小于某个数值(end)的全部素数,但是end的值不确定,可能非常大。修改后的代码如下:

#/usr/bin/env python
#coding:utf-8

__author__ = 'kikay'

import math

#判断一个数值是否是素数
def isPrime(iNum):
    if iNum>1:
        if iNum==2:
            return True
        if iNum%2==0:
            return False
        for i in range(3,int(math.sqrt(iNum)+1),2):
            if iNum%i==0:
                return False
        return True
    return False

#列表生成方法
def GetByList(iList):
    res=list()
    for i in iList:
        if isPrime(i):
            res.append(i)
    return res

#生成器
def GetByGenerator(iList):
    return (i for i in iList if isPrime(i))

#生成器(含条件)
def GetByGenerator(iList,iEnd):
    return (i for i in iList if isPrime(i) and iEnd>i)

if __name__=='__main__':
    end=100
    L=xrange(0,end)
    #res=GetByList(L)
    res=GetByGenerator(L,end)
    print type(res)
    for i in res:
        print i,

运行结果:

<type 'generator'>
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

0x03 生成器函数

上面较好地解决了提出的问题,但是我们可以看到,函数输入参数中还是带了1个List对象(或者Generator对象),计算完毕后返回了1个Generator对象。那么有没有可能不需要输入Generator对象呢?答案是可以的,我们可以进一步定义Generator函数,改写后的代码如下:

#/usr/bin/env python
#coding:utf-8

__author__ = 'kikay'

import math

#判断一个数值是否是素数
def isPrime(iNum):
    if iNum>1:
        if iNum==2:
            return True
        if iNum%2==0:
            return False
        for i in range(3,int(math.sqrt(iNum)+1),2):
            if iNum%i==0:
                return False
        return True
    return False

#列表生成方法
def GetByList(iList):
    res=list()
    for i in iList:
        if isPrime(i):
            res.append(i)
    return res

#生成器
def GetByGenerator(iList):
    return (i for i in iList if isPrime(i))

#生成器(含条件)
def GetByGenerator(iList,iEnd):
    return (i for i in iList if isPrime(i) and iEnd>i)

#生成器函数
def GetByGenerator_Fun(iEnd):
    i=0
    while i<iEnd:
        if isPrime(i):
            yield i
        i+=1

if __name__=='__main__':
    #模拟最大值
    end=100
    #L=xrange(0,end)
    #res=GetByList(L)
    #res=GetByGenerator(L,end)
    #print type(res)
    f=GetByGenerator_Fun(end)
    print type(f)
    for i in f:
        print i

运行结果:

<type 'generator'>
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

上面有一处yield。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行(把函数改成generator后,基本上不会用next()来调用它,而是直接使用for循环来迭代,见上面的例子)。

0x04 协程

前面我们演示了关于素数的例子。现在我们改下需求:找出比某个数的等比数列大的最小素数(比如num=10,我们要生成比“10,100,1000,10000,…”大的最小素数)。先看下用协程思路来完成的代码:

#/usr/bin/env python
#coding:utf-8

__author__ = 'kikay'

import math

#判断一个数值是否是素数
def isPrime(iNum):
    if iNum>1:
        if iNum==2:
            return True
        if iNum%2==0:
            return False
        for i in range(3,int(math.sqrt(iNum)+1),2):
            if iNum%i==0:
                return False
        return True
    return False

#生成器函数
def GetByCoroutine(iNum):
    while True:
        if isPrime(iNum):
            iNum=yield iNum
        iNum+=1

#获取最小的素数
def GetMinValue(iIter,iBase):
    f=GetByCoroutine(iBase)
    #启动第一次
    f.send(None) #或者f.next()
    #开始迭代
    for i in xrange(1,iIter):
        #传递等比数列的下一个数字
        iTemp=f.send(iBase**i)
        print iTemp,

if __name__=='__main__':
    #基数
    base=10
    #等比数列迭代次数
    iter=5
    GetMinValue(iter,base)

上面的过程就是利用协程来完成的。有几点做下说明:

(1)iNum=yield iNum

yield关键字返回了当前iNum的值,而iNum=yield iNumde 意思是生成器函数将“冻结”,直到收到“激活”值,即收到从调用者传来的值(通过send、next等),并赋值给iNum。

(2)iTemp=f.send(iBase**i)

send首先取回被调用的生成器函数yield返回的值(用iTemp接收),同时给yield赋值(iBase**i),并激活生成器函数的下次迭代。

(3)f.send(None)

当用send来“启动”生成器函数的时候,必须首先发送None(或者调用next函数)。因为如果不发送,生成器函数无法去接收我们下面发送的值(iTemp=f.send(iBase**i))。

从上面的例子可以看出,协程比较类似于多线程,但是协程的特点是其在1个线程中执行,不需要来回切换线程,极大减小了消耗。同时,由于是在同一线程中执行,自然不需要使用同步锁(参考博客:Python学习总结笔记(3)–多线程与线程同步)。协程在同一个线程中执行,在多CPU环境下,可以利用多进程+协程,既充分利用多核,又充分发挥协程优点,可以获得极大效率。

0x05后记

生成器和协程是非常强大的工具。可以简化我们的代码逻辑,更可以降低内存消耗,提高代码质量与效率。
Python对协程的支持只是提供了基本的功能,并不完全。诸如gevent库等提供了完善的协程功能,在后续博客中会发布相关内容。