这一章开始介绍生成器函数和
yield关键字,以及Python中的协程的概念。
yield关键字
当我们调用一个普通的函数时,执行过程从第一条语句开始,直到碰到一个
return语句或者遇到一个异常抛出, 再或者到了函数最后一条语句(实际上相对于一个隐式的
returnNone)的时候结束。一旦这个函数返回后将控制权交还给它的调用者,它里面所有的局部变量值都消失了,当你重新调用它的时候,一切又将重新开始。
这就是我们通常意义上面所认识的函数(或者说是子程序),但有时候我们需要创建某个函数,它并不简单的返回一个值, 而是可以不断的释放一个值的序列。那么这个特殊的函数就需要能够“保存”它的状态。
我刚刚用了“释放一个值的序列”这种说法,是因为我刚刚所假定的那个函数并不是普通的返回一个值。
return表明函数将控制权返回给被调用时的那个点。而
yield表示这个控制权的转义只是暂时的,将来我还可以重获控制权。
在Python中,有这种能力的“函数”被称为生成器,它们相当有用。生成器(yield语句)刚开始被引入进来主要是用来方便的生成序列值。
为了更好的理解生成器的作用,我们先来看一个例子,看看怎样来生成一个序列。
注意:很多时候我会将生成器叫做协程,后面我会用这个术语,所有的协程其实就是生成器, Python官方只定义了生成器这个名字,协程是另一种叫法。
有趣的素数
假设你老板叫你写一个函数,接受一个整数列表为参数,然后输出一个只包含素数的Iterable(一次返回一个成员的对象)。
这个很简单嘛
defget_primes(input_list):
result_list=list()
forelementininput_list:
ifis_prime(element):
result_list.append()
returnresult_list
# or better yet...
defget_primes(input_list):
return(elementforelementininput_listifis_prime(element))
# not germane to the example, but here's a possible implementation of
defis_prime(number):
ifnumber>1:
ifnumber==2:
returnTrue
ifnumber%2==0:
returnFalse
forcurrentinrange(3,int(math.sqrt(number)+1),2):
ifnumber%current==0:
returnFalse
returnTrue
returnFalse
上面任何一个
get_primes()都能满足要求,所以我们告诉老板任务完成了。老板测试后觉得很满意,符合她的期望。
处理无限序列
不过麻烦又来了,过了几天我们老板又跑来了,说她要遇到个小麻烦,她想这个
get_primes()能处理超大的列表, 这个列表太多以至于直接在内存中初始化会撑爆内存。所以她希望用一个起始值
start来调用这个函数,然后返回所有的比它大的素数。(可能她要尝试去解决欧拉猜想)
这时候我们得重新设计下
get_primes()函数了,很明显我们不可能直接返回一个无限的列表 (但是又经常能碰到需要操作无限列表的情况)。普通函数也许是行不通了
在我们放弃之前,让我们思考一下这里面最大的障碍是什么。其实就是普通函数只有一次机会返回结果, 因此我们必须把所有结果汇总后返回。但有什么办法呢?函数只能一次返回啊。
要是有特殊函数不是这样的呢,设想下如果
get_primes()函数一次只返回下一个值,我们就根本不需要构建列表了。没有列表也就占用不了内存,而老板只是想在结果上面进行迭代操作,她并不在意你以什么形式返回来。
不幸的是,这个看上去好像不可能实现,就算我们有个魔法函数允许我们从n循环到无穷大
defget_primes(start):
forelementinmagical_infinite_range(start):
ifis_prime(element):
returnelement
然后我们像下面这样调用这个函数:
defsolve_number_10():
total=2
fornext_primeinget_primes(3):
ifnext_prime<2000000:
total+=next_prime
else:
print(total)
return
很明显,
get_primes()函数遇到3的时候就返回了,在第4行,但是第二次调用的时候,它又返回了3。我们需要一种机制,下一次再调用它的时候,能从第4行开始获取下一个数,但是你再次调用函数时, 它又重新开始运行,因为函数只有一个入口,就是第一行。
走进生成器
这类问题非常普遍,Python于是引入了新的一种机制来解决它:生成器。一个生成器负责生产数据, 可以通过简单的调用生成器函数来创建一个生成器。
一个生成器函数定义跟普通函数类似,唯一区别是,它通过
yield关键字生产数据,而不是用
return关键字返回。如果在一个
def函数定义内有出现了一个
yield关键字,那么这个函数就变成了生成器函数(就算它还包含return语句)。
生成器函数可创建生成器迭代器,其实就是我们通常所说的生成器。请记住生成器是一类特殊的迭代器。要成为一个迭代器,生成器就必须定义一些特殊方法,其中一个就是
__next__(), 要从生成器中获取下一个元素,我们使用同样的内置函数
next()
因此,当你使用
next()调用一个生成器的时候,生成器负责传会下一个值,它会把
yield后面的值传过来。所以记住这句话:
yield相对于生成器函数的
return
下面是一个简单的生成器函数:
defsimple_generator_function():
yield1
yield2
yield3
两种使用方法:
forvalueinsimple_generator_function():
print(value)
our_generator=simple_generator_function()
next(our_generator)
next(our_generator)
next(our_generator)
请注意,这里的
simple_generator_function仍然是一个函数只不过由于调用这个函数后会返回一个生成器对象, 所以我们叫这个函数为生成器函数。
神奇的背后
那么这是怎么一回事呢。当生成器函数调用
yield语句的时候,生成器函数的状态被冻结,所有变量值都被保存, 下一条要执行的语句被记录下来,直到下一次的
next()调用来临。一旦再遇到
next()调用,生成器函数就被激活,而如果
next()永远不再调用,那么最后记录的状态也就被丢弃了。
我们使用生成器函数重新改写下
get_primes()函数,这次我们不再需要
magical_infinite_range()函数了。直接使用
while循环来创建我们的无限序列:
defget_primes(number):
whileTrue:
ifis_prime(number):
yieldnumber
number+=1
如果一个生成器函数调用了
return语句或者到了最后一条语句,就会抛出一个
StopIteration异常, 它表明不能再调用
next()了。因此我们使用
while循环就能保证它永远不会达到最后, 所以只要我们不断的调用
next(),它就能源源不断的为我们产生新的值。这也是我们在处理无限序列时常用的做法。
程序控制流
我们重新来看看代码
solve_number_10,它在调用
get_primes()时究竟发生了什么:
defsolve_number_10():
total=2
fornext_primeinget_primes(3):
ifnext_prime<2000000:
total+=next_prime
else:
print(total)
return
我们一步步来,当我们使用for循环调用
get_primes(3)时候,其实就是不断的使用
next()调用它。程序控制流进入
get_primes(),我们来分析下这个函数执行流
第3行我们碰到了while循环语句
if条件判断碰到数字3(是一个素数满足条件)
yield数字3并将控制权返回给
solve_number_10
然后,我们再去看
solve_number_10:
数字3从for循环中拿到
for循环将
next_prime设置成3
next_prime被加到
total上
for循环再次从
get_primes请求数据
这时候,控制不会直接从
get_prime()函数第1行语句进入,而是从第5行恢复运行,那也是我们离开的地方。
defget_primes(number):
whileTrue:
ifis_prime(number):
yieldnumber
number+=1
最重要的是,number的值还保存着之前的3。请记住,
yield会将数字传递给
next()调用者, 并同时保存生成器函数的状态,包括它所有内部的局部变量值。这时候,number的值+1变成4,然后重新while循环,我们不断的增加这个number的值直到它是一个素数5, 这时候,
yield将这个5传递给
solve_number_10中的
for循环。这个过程不断的重复,直到for循环停止了(遇到素数大于2000000)。
更强大的生成器
在PEP 342中,我们可以向生成器传递数据,它让生成器有了三种能力:可以生产一个数(前面讲过)
接受一个数,这个数是任意的
在一条语句中生产一个数并同时接受另一个数
为了说明怎样向生成器发送数据,还是刚刚那个素数例子,这次我们并不是简单的返回比
number大的素数, 而是找出大于这个数的连续指数最小的素数。比如对于10,我们要找出比10大的最小素数,然后是比100大的最小素数,然后是比1000大的最小素数,以此类推。
defget_primes(number):
whileTrue:
ifis_prime(number):
number=yieldnumber
number+=1
解释下
get_primes()中的
number=yieldnumber这句话,它表示,把
number发送出去, 然后当有人向我发送值的时候就把这个值设置成左边的
number。这样我们就能每次进入
get_prime()下次执行时,将
number设置成我们想要的值,而不是离开时它自己保存的值。
然后我们再来改写调用它的函数:
defprint_successive_primes(iterations,base=10):
prime_generator=get_primes(base)
prime_generator.send(None)
forpowerinrange(iterations):
print(prime_generator.send(base**power))
两件事需要说明下:我们打印
generator.send的结果,因为
send做了两件事,一是给生成器发送数据,并同时返回生成器生产的值。
注意到这一行
prime_generator.send(None),当我们使用
send来启动一个生成器 (也就是从生成器第一行开始执行一直到碰到第一个
yield语句),你必须发送
None。这个是有必要的,因为生成器函数定义的第一条语句并不是yield,因此要是我们发送一个真实的数据过去,没东西可以接受啊。一旦这个生成器启动了,我们就能给它发送数据了。
在来看一个send例子
if__name__=='__main__':
defmyfunc():
print'start myfunc...',
m=yield111
print'get m = %s'%m
n=yield222
print'get n = %s'%n
yield
defcaller():
c=myfunc()# 调用生成器函数获取到生成器c
result=c.next()# 相当于c.send(None)
print'(1) result=%s'%result
result=c.send('Hello')
print'(2) result=%s'%result
c.send('Goodbye')
caller()
我用图片方式标注几个重要的执行点,然后分析一下整个执行流程:
执行到
result=c.next()的时候,这个会执行这个生成器函数直到遇见
yield语句,yield语句返回值111, 控制权里面回到caller()里来,将返回值赋值给result,那么打印"(1) result=111"。
然后执行
c.send('Hello'),这一步会跳到myfunc()的上次保存点,m接收值
Hello(上一次是从
m=yield111那个地方跳出来的), 然后打印"get m = Hello",直到运行到又碰到yield语句
yield222。
然后控制权返回到caller()来,并将result设置成222,打印"(2) result=222"。
最后一句
c.send('Goodbye'),又跳到myfunc()中的上次保存点去,n接收值'Goodbye',然后打印
getn=Goodbye, 它会继续往下执行一定要找到一个
yield语句并返回到caller()中,否则抛出
StopIteration异常。
Python中的协程
现在我们已经对生成器和
yield原理有了理解。让我们再看看
yield还有什么是可以做的。尽管send确实可以像上面那样用,但是生产简单的序列的时候我们基本不会那样用。下面再举一个例子看看send使用的常见场景:
importrandom
defget_data():
"""Return 3 random integers between 0 and 9"""
returnrandom.sample(range(10),3)
defconsume():
"""Displays a running average across lists of integers sent to it"""
running_sum=0
data_items_seen=0
whileTrue:
data=yield# 负责接收数据
data_items_seen+=len(data)
running_sum+=sum(data)
print('The running average is {}'.format(running_sum/float(data_items_seen)))
defproduce(consumer):
"""Produces a set of values and forwards them to the pre-defined consumer
function"""
whileTrue:
data=get_data()
print('Produced {}'.format(data))
consumer.send(data)
yield# 变成一个生成器函数
if__name__=='__main__':
consumer=consume()
consumer.send(None)
producer=produce(consumer)
for_inrange(10):
print('Producing...')
next(producer)
呃,竟然是生产者-消费者模式,这种方式其实就是协程异步方案,看上去好高级哦,不过原理很简单,应该看得懂吧。
Python的线程是系统级进程,线程间切换开销不小,而且Python在执行多线程时默认加了一个全局解释器锁(GIL), 所以Python的多线程其实是串行的,并不能利用多核的优势。而哪怕在Java、C#这样的语言中,多线程真的是并发的, 虽然可以利用多核优势,但由于线程的切换是由调度器控制的,不论是用户级线程还是系统级线程, 调度器都会由于IO操作、时间片用完等原因强制夺取某个线程的控制权,又由于线程间共享状态的不可控性, 同时也会带来安全问题。所以我们在写多线程程序的时候都会加各种锁,很是麻烦,一不小心就会造成死锁,而且锁对性能,总是有些影响的。
然后说到协程,与线程的抢占式调度不同,它是协作式调度,之前忘了看哪个技术分享视频说过, 纤程(跟协程一样,只是叫法不同),大致上就是一种可控制的回调,是比线程更轻量级的一种实现异步编程的方式。协程在Python中可以用generator(生成器)实现,生成器主要由yeild关键字实现。
而用yeild实现协程的话,其实有挺多不同的方式,不过大致的思想都是用yeild来暂停当前执行的程序, 转而执行另一个,再在恰当的时候(可以控制)回来执行。用法很灵活。
produce和consumer协同合作,过程清晰可控,在一个线程中执行,不需要锁,也就不会出现死锁情况。
其实Python用生成器对协程的实现并不完整,不过可以用一些第三方库,比如greenlet,毕竟Python最大的优势就是库多嘛。
总结
看完这篇文章,我希望你能记住下面这几点就足够了:生成器是用来生产序列的
yield就相对于是生成器函数的
return
yield额外做的工作就是保存了生成器函数的状态
一个生成器是一个特殊的迭代器
和迭代器类似,我们可以通过在生成器上面使用
next()或者是
for循环来获取下一个值
我希望这篇文章能对你理解生成器有帮助,你应该知道了它是什么,为什么它这样有用,以及怎样使用它。如果你之前就熟悉它,我希望现在对于它不会存在误解了。
这一章开始介绍生成器函数和
yield关键字,以及Python中的协程的概念。
yield关键字
当我们调用一个普通的函数时,执行过程从第一条语句开始,直到碰到一个
return语句或者遇到一个异常抛出, 再或者到了函数最后一条语句(实际上相对于一个隐式的
returnNone)的时候结束。一旦这个函数返回后将控制权交还给它的调用者,它里面所有的局部变量值都消失了,当你重新调用它的时候,一切又将重新开始。
这就是我们通常意义上面所认识的函数(或者说是子程序),但有时候我们需要创建某个函数,它并不简单的返回一个值, 而是可以不断的释放一个值的序列。那么这个特殊的函数就需要能够“保存”它的状态。
我刚刚用了“释放一个值的序列”这种说法,是因为我刚刚所假定的那个函数并不是普通的返回一个值。
return表明函数将控制权返回给被调用时的那个点。而
yield表示这个控制权的转义只是暂时的,将来我还可以重获控制权。
在Python中,有这种能力的“函数”被称为生成器,它们相当有用。生成器(yield语句)刚开始被引入进来主要是用来方便的生成序列值。
为了更好的理解生成器的作用,我们先来看一个例子,看看怎样来生成一个序列。
注意:很多时候我会将生成器叫做协程,后面我会用这个术语,所有的协程其实就是生成器, Python官方只定义了生成器这个名字,协程是另一种叫法。
有趣的素数
假设你老板叫你写一个函数,接受一个整数列表为参数,然后输出一个只包含素数的Iterable(一次返回一个成员的对象)。
这个很简单嘛
defget_primes(input_list):
result_list=list()
forelementininput_list:
ifis_prime(element):
result_list.append()
returnresult_list
# or better yet...
defget_primes(input_list):
return(elementforelementininput_listifis_prime(element))
# not germane to the example, but here's a possible implementation of
defis_prime(number):
ifnumber>1:
ifnumber==2:
returnTrue
ifnumber%2==0:
returnFalse
forcurrentinrange(3,int(math.sqrt(number)+1),2):
ifnumber%current==0:
returnFalse
returnTrue
returnFalse
上面任何一个
get_primes()都能满足要求,所以我们告诉老板任务完成了。老板测试后觉得很满意,符合她的期望。
处理无限序列
不过麻烦又来了,过了几天我们老板又跑来了,说她要遇到个小麻烦,她想这个
get_primes()能处理超大的列表, 这个列表太多以至于直接在内存中初始化会撑爆内存。所以她希望用一个起始值
start来调用这个函数,然后返回所有的比它大的素数。(可能她要尝试去解决欧拉猜想)
这时候我们得重新设计下
get_primes()函数了,很明显我们不可能直接返回一个无限的列表 (但是又经常能碰到需要操作无限列表的情况)。普通函数也许是行不通了
在我们放弃之前,让我们思考一下这里面最大的障碍是什么。其实就是普通函数只有一次机会返回结果, 因此我们必须把所有结果汇总后返回。但有什么办法呢?函数只能一次返回啊。
要是有特殊函数不是这样的呢,设想下如果
get_primes()函数一次只返回下一个值,我们就根本不需要构建列表了。没有列表也就占用不了内存,而老板只是想在结果上面进行迭代操作,她并不在意你以什么形式返回来。
不幸的是,这个看上去好像不可能实现,就算我们有个魔法函数允许我们从n循环到无穷大
defget_primes(start):
forelementinmagical_infinite_range(start):
ifis_prime(element):
returnelement
然后我们像下面这样调用这个函数:
defsolve_number_10():
total=2
fornext_primeinget_primes(3):
ifnext_prime<2000000:
total+=next_prime
else:
print(total)
return
很明显,
get_primes()函数遇到3的时候就返回了,在第4行,但是第二次调用的时候,它又返回了3。我们需要一种机制,下一次再调用它的时候,能从第4行开始获取下一个数,但是你再次调用函数时, 它又重新开始运行,因为函数只有一个入口,就是第一行。
走进生成器
这类问题非常普遍,Python于是引入了新的一种机制来解决它:生成器。一个生成器负责生产数据, 可以通过简单的调用生成器函数来创建一个生成器。
一个生成器函数定义跟普通函数类似,唯一区别是,它通过
yield关键字生产数据,而不是用
return关键字返回。如果在一个
def函数定义内有出现了一个
yield关键字,那么这个函数就变成了生成器函数(就算它还包含return语句)。
生成器函数可创建生成器迭代器,其实就是我们通常所说的生成器。请记住生成器是一类特殊的迭代器。要成为一个迭代器,生成器就必须定义一些特殊方法,其中一个就是
__next__(), 要从生成器中获取下一个元素,我们使用同样的内置函数
next()
因此,当你使用
next()调用一个生成器的时候,生成器负责传会下一个值,它会把
yield后面的值传过来。所以记住这句话:
yield相对于生成器函数的
return
下面是一个简单的生成器函数:
defsimple_generator_function():
yield1
yield2
yield3
两种使用方法:
forvalueinsimple_generator_function():
print(value)
our_generator=simple_generator_function()
next(our_generator)
next(our_generator)
next(our_generator)
请注意,这里的
simple_generator_function仍然是一个函数只不过由于调用这个函数后会返回一个生成器对象, 所以我们叫这个函数为生成器函数。
神奇的背后
那么这是怎么一回事呢。当生成器函数调用
yield语句的时候,生成器函数的状态被冻结,所有变量值都被保存, 下一条要执行的语句被记录下来,直到下一次的
next()调用来临。一旦再遇到
next()调用,生成器函数就被激活,而如果
next()永远不再调用,那么最后记录的状态也就被丢弃了。
我们使用生成器函数重新改写下
get_primes()函数,这次我们不再需要
magical_infinite_range()函数了。直接使用
while循环来创建我们的无限序列:
defget_primes(number):
whileTrue:
ifis_prime(number):
yieldnumber
number+=1
如果一个生成器函数调用了
return语句或者到了最后一条语句,就会抛出一个
StopIteration异常, 它表明不能再调用
next()了。因此我们使用
while循环就能保证它永远不会达到最后, 所以只要我们不断的调用
next(),它就能源源不断的为我们产生新的值。这也是我们在处理无限序列时常用的做法。
程序控制流
我们重新来看看代码
solve_number_10,它在调用
get_primes()时究竟发生了什么:
defsolve_number_10():
total=2
fornext_primeinget_primes(3):
ifnext_prime<2000000:
total+=next_prime
else:
print(total)
return
我们一步步来,当我们使用for循环调用
get_primes(3)时候,其实就是不断的使用
next()调用它。程序控制流进入
get_primes(),我们来分析下这个函数执行流
第3行我们碰到了while循环语句
if条件判断碰到数字3(是一个素数满足条件)
yield数字3并将控制权返回给
solve_number_10
然后,我们再去看
solve_number_10:
数字3从for循环中拿到
for循环将
next_prime设置成3
next_prime被加到
total上
for循环再次从
get_primes请求数据
这时候,控制不会直接从
get_prime()函数第1行语句进入,而是从第5行恢复运行,那也是我们离开的地方。
defget_primes(number):
whileTrue:
ifis_prime(number):
yieldnumber
number+=1
最重要的是,number的值还保存着之前的3。请记住,
yield会将数字传递给
next()调用者, 并同时保存生成器函数的状态,包括它所有内部的局部变量值。这时候,number的值+1变成4,然后重新while循环,我们不断的增加这个number的值直到它是一个素数5, 这时候,
yield将这个5传递给
solve_number_10中的
for循环。这个过程不断的重复,直到for循环停止了(遇到素数大于2000000)。
更强大的生成器
在PEP 342中,我们可以向生成器传递数据,它让生成器有了三种能力:可以生产一个数(前面讲过)
接受一个数,这个数是任意的
在一条语句中生产一个数并同时接受另一个数
为了说明怎样向生成器发送数据,还是刚刚那个素数例子,这次我们并不是简单的返回比
number大的素数, 而是找出大于这个数的连续指数最小的素数。比如对于10,我们要找出比10大的最小素数,然后是比100大的最小素数,然后是比1000大的最小素数,以此类推。
defget_primes(number):
whileTrue:
ifis_prime(number):
number=yieldnumber
number+=1
解释下
get_primes()中的
number=yieldnumber这句话,它表示,把
number发送出去, 然后当有人向我发送值的时候就把这个值设置成左边的
number。这样我们就能每次进入
get_prime()下次执行时,将
number设置成我们想要的值,而不是离开时它自己保存的值。
然后我们再来改写调用它的函数:
defprint_successive_primes(iterations,base=10):
prime_generator=get_primes(base)
prime_generator.send(None)
forpowerinrange(iterations):
print(prime_generator.send(base**power))
两件事需要说明下:我们打印
generator.send的结果,因为
send做了两件事,一是给生成器发送数据,并同时返回生成器生产的值。
注意到这一行
prime_generator.send(None),当我们使用
send来启动一个生成器 (也就是从生成器第一行开始执行一直到碰到第一个
yield语句),你必须发送
None。这个是有必要的,因为生成器函数定义的第一条语句并不是yield,因此要是我们发送一个真实的数据过去,没东西可以接受啊。一旦这个生成器启动了,我们就能给它发送数据了。
在来看一个send例子
if__name__=='__main__':
defmyfunc():
print'start myfunc...',
m=yield111
print'get m = %s'%m
n=yield222
print'get n = %s'%n
yield
defcaller():
c=myfunc()# 调用生成器函数获取到生成器c
result=c.next()# 相当于c.send(None)
print'(1) result=%s'%result
result=c.send('Hello')
print'(2) result=%s'%result
c.send('Goodbye')
caller()
我用图片方式标注几个重要的执行点,然后分析一下整个执行流程:
执行到
result=c.next()的时候,这个会执行这个生成器函数直到遇见
yield语句,yield语句返回值111, 控制权里面回到caller()里来,将返回值赋值给result,那么打印"(1) result=111"。
然后执行
c.send('Hello'),这一步会跳到myfunc()的上次保存点,m接收值
Hello(上一次是从
m=yield111那个地方跳出来的), 然后打印"get m = Hello",直到运行到又碰到yield语句
yield222。
然后控制权返回到caller()来,并将result设置成222,打印"(2) result=222"。
最后一句
c.send('Goodbye'),又跳到myfunc()中的上次保存点去,n接收值'Goodbye',然后打印
getn=Goodbye, 它会继续往下执行一定要找到一个
yield语句并返回到caller()中,否则抛出
StopIteration异常。
Python中的协程
现在我们已经对生成器和
yield原理有了理解。让我们再看看
yield还有什么是可以做的。尽管send确实可以像上面那样用,但是生产简单的序列的时候我们基本不会那样用。下面再举一个例子看看send使用的常见场景:
importrandom
defget_data():
"""Return 3 random integers between 0 and 9"""
returnrandom.sample(range(10),3)
defconsume():
"""Displays a running average across lists of integers sent to it"""
running_sum=0
data_items_seen=0
whileTrue:
data=yield# 负责接收数据
data_items_seen+=len(data)
running_sum+=sum(data)
print('The running average is {}'.format(running_sum/float(data_items_seen)))
defproduce(consumer):
"""Produces a set of values and forwards them to the pre-defined consumer
function"""
whileTrue:
data=get_data()
print('Produced {}'.format(data))
consumer.send(data)
yield# 变成一个生成器函数
if__name__=='__main__':
consumer=consume()
consumer.send(None)
producer=produce(consumer)
for_inrange(10):
print('Producing...')
next(producer)
呃,竟然是生产者-消费者模式,这种方式其实就是协程异步方案,看上去好高级哦,不过原理很简单,应该看得懂吧。
Python的线程是系统级进程,线程间切换开销不小,而且Python在执行多线程时默认加了一个全局解释器锁(GIL), 所以Python的多线程其实是串行的,并不能利用多核的优势。而哪怕在Java、C#这样的语言中,多线程真的是并发的, 虽然可以利用多核优势,但由于线程的切换是由调度器控制的,不论是用户级线程还是系统级线程, 调度器都会由于IO操作、时间片用完等原因强制夺取某个线程的控制权,又由于线程间共享状态的不可控性, 同时也会带来安全问题。所以我们在写多线程程序的时候都会加各种锁,很是麻烦,一不小心就会造成死锁,而且锁对性能,总是有些影响的。
然后说到协程,与线程的抢占式调度不同,它是协作式调度,之前忘了看哪个技术分享视频说过, 纤程(跟协程一样,只是叫法不同),大致上就是一种可控制的回调,是比线程更轻量级的一种实现异步编程的方式。协程在Python中可以用generator(生成器)实现,生成器主要由yeild关键字实现。
而用yeild实现协程的话,其实有挺多不同的方式,不过大致的思想都是用yeild来暂停当前执行的程序, 转而执行另一个,再在恰当的时候(可以控制)回来执行。用法很灵活。
produce和consumer协同合作,过程清晰可控,在一个线程中执行,不需要锁,也就不会出现死锁情况。
其实Python用生成器对协程的实现并不完整,不过可以用一些第三方库,比如greenlet,毕竟Python最大的优势就是库多嘛。
总结
看完这篇文章,我希望你能记住下面这几点就足够了:生成器是用来生产序列的
yield就相对于是生成器函数的
return
yield额外做的工作就是保存了生成器函数的状态
一个生成器是一个特殊的迭代器
和迭代器类似,我们可以通过在生成器上面使用
next()或者是
for循环来获取下一个值
我希望这篇文章能对你理解生成器有帮助,你应该知道了它是什么,为什么它这样有用,以及怎样使用它。如果你之前就熟悉它,我希望现在对于它不会存在误解了。