任何复杂的概念或系统都不是凭空出现的,我们完全可以找到它的演化历程,寻根究底终会发现,其都是在一系列并不那么复杂的简单组件上发展演化而来!
by 落花僧
本文通过一系列关键概念,逐步递进理解协程。
0.并发
不管做什么事情,我们都不喜欢等待,能够越快越好。在web服务领域,说并发量,就是指一个web服务器能同一时间接收多少用户同时访问,在互联网早期因为用户量低,所以上古时候都使用多fork线程来应付同时访问的用户,但随着同一时间用户量的增加,线程的开销对服务器压力越来越大,所以出现了一些同一时间提供并发量的编程模型(思想),而协程就是其中一种。
1. iterator(迭代器)
在python中"序列"的对象都可用 iter(the_sequence_object)
变成一个iterator:
>>>iter('this is string.' )
<str_iterator at 0x1033ce9e8>
>>>iter([1,2,3])
<list_iterator at 0x1033b3f60>
>>>iter(set({1,2,3}))
<set_iterator at 0x103434048>
>>>iter(1)
TypeError: 'int' object is not iterable
int不是序列对象,不能iter, 那么问题来了,能iter又有什么用呢?
看下面(例子1-1):
a = [1, 2, 3]
a_iterator = iter(a)
while True:
try:
print(next(a_iterator))
except StopIteration:
break
这段代码遍历了列表a, 其等同于(例子1-2):
a = [1, 2, 3]
for i in a:
print(i)
在例子1-2的for循环前解释器隐式的调用了iter, 遍历循环中调用next. 目前看起来这个迭代器的作用就是将所有序列对象进行了抽象,可以统一使用next不断获取序列的下一个元素,在业务代码层次表现出来的是我们可以统一使用for in遍历操作。注意:这个序列对象不一定非得是有序的数组类型的数据结构。
2.Lazy evaluation(惰性计算)
简而言之就是用到的时候我们再去求值,比如可以写一个无限生成斐波那契数列的函数,想要多少个数字就取多少个数字,但是这个函数不会一开始就生成好某个长度的序列,而是你每次获取一个新的数字的时候,它是根据上一个状态计算这个数字,然后只保存函数当前的执行结果状态,可以一直无限获取下去。python在版本2.2引入了这个思想,这个关键词就是yield!
3. yield and Generator
yield从字面理解就是产生某个东西,它其实本质上是一个代码流程控制语句,和if, for, while这种代码控制语句是同一个层次的东西。那么它控制了什么?或者到底产生了什么了呢?答案就是:
所有包含 yield 语句的函数,都会被python编译成一个生成器(Generator)
问题又来了,啥是生成器呢?
生成器就是“惰性计算”这个概念的实现载体!通过生成器来实现“惰性计算”!
废话不多说,上代码(例子3-1)!
def fib():
a, b = 0, 1
while True:
yield a
a, b = a+b, a
fib_generator = fib()
print(type(fib_generator)) # type一下其类型会输出:generator
print(next(fib_generator)) # 1
print(next(fib_generator)) # 1
print(next(fib_generator)) # 2
print(next(fib_generator)) # 3
print(next(fib_generator)) # 5
print(next(fib_generator)) # 8
这个fib_generator就是一个生成器,可以无穷尽产生fibnacci数列,你想要多少就可以取多少(想象如果你自己的银行卡也是一个生成器那该多美好 -_-!)
回想一下在讲 迭代器 的概念的时候,我们说任何迭代器都可以使用for in, 而生成器是一种特殊的迭代器,这个fib_generator当然也是可以for in的。
生成器的迭代和我们对一个list_iterator的迭代有什么本质的区别呢?答案就是yield引入的惰性计算,只有你在next(generator)的时候才会去计算出相应的值,而你在next(list_iterator)的时候,里面的值是早就存在的。
总结一下生成器的具体规则:
- 所有包含yield的函数都会被python解释器“解释”成一个生成器。
- 生成器中有yield语句的地方,每次next(generator)代码按逻辑顺序执行到yield处,都会返回yield后面表达式的值,然后暂停等待下一个next,继续按逻辑顺序执行。
承接上面的例子3-1,到这里,逻辑上我们可以执行这样的代码:
next(fib_generator)
do_something()
next(fib_generator)
因为生成器保留函数上一次执行的堆栈信息,所以我们在两次next之间完全可以执行其他代码!现在请你思考一下,这种功能有什么用?
····
你隐约是不是想到了什么,但又不是太清晰?没关系,我们继续复盘这种逻辑演化。
4.generator.send() and Coroutine
在python 2.5版本中,为生成器加入了一个至关重要的API,它就是send方法。这个send干什么用呢?
>>> def one_coroutine():
got_string = yield
print('i got:', got_string)
>>> c = one_coroutine()
>>> next(c) # 启动生成器,简单理解就是生成器需要执行到yield处暂停。
>>> c.send('something')
i got: something
StopIteration Traceback (most recent call last)
注意这里的yield和之前的用法有个重大区别,那就是之前的用法是 yield+表达式,此处yield是一个赋值语句的右边,表示等待一个值,将其赋值给左边。我们使用send方法为生成器发送了一个字符串,然后生成器立即获取字符串并执行了生成器后面的代码。请回忆一下之前提到的生成器执行规则,会遇到yield暂停,这里代码执行下去不能再遇到新的yield,所以生成器抛出StopIteration,结束了生成器,我们可以把代码改一下,然后就能不断的给生成器发送字符串了:
def one_coroutine():
while True:
got_string = yield
print('i got:', got_string)
这里我们已经完成了一个最简洁的协程。
待续:彻底搞懂python协程-第二篇(关键词5-10)