在调用普通函数时,程序会中断调用代码运行,切换到调用函数的第一行代码开始执行,到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库等提供了完善的协程功能,在后续博客中会发布相关内容。