2. 生成器
引文
在Python的技术类面试中,迭代器、生成器与装饰器相关的话题通常是必问的;
熟练掌握具体的知识也有助于我们写出好更优雅的代码;
这里参考相关的资料详细地学习下,备以后查阅。
本文是迭代器,生成器和装饰器系列的第二篇文章,主要讲解生成器;
探索生成器
生成器是一类特殊的函数,不同于一般的函数去返回一个值;
生成器返回一个迭代器,可用于返回一个数据流;
一般的函数在调用完成,返回了结果后;函数内部命名空间以及创建的局部变量会全部销毁;
下次调用时,会在一个新的命名空间,同时声明新的局部变量;
而生成器不会再每次调用后去清理和释放掉,下次调用时还可以从前次退出的地方恢复;
所以生成器可以看作是一个可以恢复的函数;
一个简单的例子:
def generator_demo(N):
for i in range(N):
yield i
yield是生成器函数的关键字,解释器检测到函数内部有yield关键字时,就认定此函数是一个“生成器函数”;
在调用生成器函数时,它不返回一个值,而是返回一个支持迭代的“生成器对象”;
生成器也是一个迭代器;但只能迭代一次;
而且生成器的返回值只在需要时才会计算;
对生成器进行迭代操作时,在执行到yield时,与return类似,这里也会返回i的值;
但生成器的执行状态会挂起和内部的局部变量会被保留;在下次执行__next__()(py2中为next())作时,会恢复继续执行;最终执行完毕时,会抛出StopIteration异常退出;
gen = generator_demo(5)
next(gen) # 0
next(gen) # 1
...
next(gen) # 4
next(gen) # StopIteration
py3支持在生成器函数内部直接return value,这相当于抛出了一个StopIteration(value);
py2不支持在生成器函数内部写return;
使用send方法,还可以向生成器内部传值;
使用throw方法,可以向生成器抛出一个异常,若对异常进行捕获处理或者抛出其他异常,则异常将传递给调用者;并且生成器的也将终止执行;
使用close方法,可以即刻关闭生成器;
>>> def echo(value=None):
... print("Execution starts when 'next()' is called for the first time.")
... try:
... while True:
... try:
... value = (yield value)
... except Exception as e:
... value = e
... finally:
... print("Don't forget to clean up when 'close()' is called.")
...
>>> generator = echo(1)
>>> print(next(generator))
Execution starts when 'next()' is called for the first time.
1
>>> print(next(generator))
None
>>> print(generator.send(2))
2
>>> generator.throw(TypeError, "spam")
TypeError('spam',)
>>> generator.close()
Don't forget to clean up when 'close()' is called.
协程
协程是可以在多个地方挂起和恢复执行的,更一般形式的程序;
相比于多进程和多线程技术,协程任务切换的花销更少;
而且协程的代码更便于阅读;
可以用生成器模拟协程;
最初,Python中没有原生的协程,主流的实现包括:
- 用C实现协程调度,以
gevent为代表,并替换了原生的阻塞IO为异步; - 使用生成器模拟,以
Tornado为代表;
Python3.4后,官方加入了异步编程,Python3.5后,语法层面实现了协程,关键字为async和await;
下面给出一个简单的协程示例:
#!/usr/bin/env python2
from collections import deque
def task(name, times):
for i in xrange(times):
yield
print name, i
class Runner(object):
def __init__(self, tasks):
self.tasks = deque(tasks)
def next(self):
self.tasks.pop()
def reload(self, task):
self.tasks.appendleft(task)
def run(self):
while len(self.tasks):
tsk = self.next()
try:
next(tsk)
except StopIteration:
pass
else:
self.reload(tsk)
if __name__ == '__main__':
Runner([
task('test', 3),
task('task', 4)
]).run()
上述代码中,task函数定义了一个任务,这个任务被拆分多个小块,挂起和恢复的点为yield所在的位置;
下面的Runner类用于启动一组task并且会进行任务的调度;
在任务组的执行过程中,并不会有任何单个任务阻塞,从调度器来看所有的任务是并行执行的;
除了并行执行的需求外,异步执行也是常见的需求;
使用协程模拟异步的思路:
- 创建消息队列用于维护异步IO的状态;
- 任务中异步IO触发,自身挂起,向消息队列中插入一条记录;
- 以轮询或epoll库的方式来获知异步IO的状态改变;
- 从消息队列中取出消息,并恢复协程函数;
代码如下:
#!/usr/bin/env python2
import time
events_list = []
class Event(object):
def __init__(self, *args):
self.callback = lambda : None
events_list.append(self)
def set_callback(self, callback)
self.callback = callback
def is_ready(self):
result = self._is_ready()
if result
self.callback()
return result
def _is_ready(self):
raise NotImplemented()
class SleepEvent(Event):
def __init__(self, timeout):
self.timeout = timeout
self.start_time = time.time()
super(SleepEvent, self).__init__(timeout)
def _is_ready(self):
return time.time() - self.start_time >= self.timeout
def sleep(timeout):
return SleepEvent(timeout)
def _next(task):
try:
event = next(task)
event.set_callback(lambda : _next(task))
except StopException:
pass
def run(tasks):
for e in tasks:
_next(e)
while len(events_list)
for e in events_list:
if e.is_ready():
events_list.remove(e)
break
def task(name, timeout):
print name, 0
sleep(timeout)
print name, timeout
if __name__ == '__main__':
run([
task('test', 2),
task('task', 3)
])
在上述代码中,将原本的阻塞sleep替换为一个yield返回一个sleep事件并挂起;利用run启动一系列的任务,并用_next预处理每一个任务——实际上是激活每一个任务函数返回的生成器,并为返回的事件对象绑定回调函数;
当每次在task中遭遇sleep时,会返回一个SleepEvent对象并将函数挂起;
而SleepEvent对象在初始化创建时就会自动把自己添加到一个全局的消息队列中,之后会在run中的while循环中检查是否满足条件is_ready——也就是是否触发异步io事件;
若满足条件,事情绑定的回调函数会被调用并在回调函数中恢复task的执行,并将当前的异步事件从队列中删去;整个过程会一直持续下去,直到所有的事件都被处理,消息队列为空时;
上述是利用协程来模拟异步IO;很显然,协程可以很好地实现异步的需求;
总结
生成器是一类特殊的函数,每次调用会返回一个结果,而且保留函数内部的执行状态;
下次调用会从上次挂起的地方继续执行;生成器只有在执行到函数底部或者抛出StopIteration才会终止;
协程是可以在多个地方挂起和恢复执行的。可以用生成器来模拟协程,相比于多线程与多进程,协程以更高效更灵活的方式实现任务的切换;
浙公网安备 33010602011771号