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中没有原生的协程,主流的实现包括:

  1. 用C实现协程调度,以gevent为代表,并替换了原生的阻塞IO为异步;
  2. 使用生成器模拟,以Tornado为代表;

Python3.4后,官方加入了异步编程,Python3.5后,语法层面实现了协程,关键字为asyncawait

下面给出一个简单的协程示例:

#!/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并且会进行任务的调度;
在任务组的执行过程中,并不会有任何单个任务阻塞,从调度器来看所有的任务是并行执行的;

除了并行执行的需求外,异步执行也是常见的需求;

使用协程模拟异步的思路:

  1. 创建消息队列用于维护异步IO的状态;
  2. 任务中异步IO触发,自身挂起,向消息队列中插入一条记录;
  3. 以轮询或epoll库的方式来获知异步IO的状态改变;
  4. 从消息队列中取出消息,并恢复协程函数;

代码如下:

#!/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才会终止;

协程是可以在多个地方挂起和恢复执行的。可以用生成器来模拟协程,相比于多线程与多进程,协程以更高效更灵活的方式实现任务的切换;

参考资料


  1. https://docs.python.org/3/howto/functional.html#generators
  2. 生成器与协程 http://hsfzxjy.github.io/python-generator-coroutine/

posted on 2017-06-02 00:20  yabzhang  阅读(167)  评论(0)    收藏  举报

导航