代码在这里
第二部分 : 协程,管道和数据流
协程可以用来构建管道,只用把协程串起来并通过send推送数据就行了
这种管道(由协程构成的)需要要一个初始的数据源(一个生产者),
源驱动了整个的管道链
像这样:
def source(target):
while not done:
item = produce_an_item() ...
target.send(item)
target.close()
数据源一般不是个协程。
管道结束点
管道必须要有一个结束点(下水道)
send()
-> coroutine -> 下水道(sink)
这个下水道的作用是收集所有发送给它的数据并处理
(即作为管道的尾端,不再向下一链条传递,必要时处理close异常)
@coroutine
def sink():
try:
while True:
item = (yield)
...
except GeneratorExit:
# Done ...
使用协程的改进版本的仿 ‘tail -f ‘命令
首先是协程的例行公事:
import functools
def coroutine(function):
@functools.wraps(function)
def wrapper(*args, **kwargs):
generator = function(*args, **kwargs)
next(generator) #prime the generator
return generator
return wrapper
数据生成源
import time
def follow(the file, target):
thefile.seek(0,2) # Go to the end of the file
while True:
line = thefile.readline()
if not line:
time.sleep(0.1) # Sleep briefly
continue
target.send(line)
这个下水道仅仅打印收到的数据
@coroutine
def printer():
while True:
line = (yield)
print(line, end=‘’)
关键点就在于follow函数读取数据并推送给printer()协程
f=open(‘a’)
follow(f, printer())
处于管道中间部分(既接收又推送)的协程一般是用来
完成变换,过滤,路由等功能。
@coroutine
def filter(target):
while True:
item = (yield)
# Receive an item
# Transform/filter item
...
# Send it along to the next stage target.send(item)
grep filter coroutine
@coroutine
def grep(pattern,target):
while True:
line = (yield) # Receive a line
组装起来(很像lisp)
f = open("access-log")
follow(f,
grep('python',
printer()))
生成器与协程组成管道的不同之处在于,
生成器是基于迭代器的而协程使用send():
generator---(迭代器)->generator---(迭代器)->
coroutine--send()->coroutine--send()->coroutine--send()->
另外,可以使用协程将数据发送至多个方向
数据源只是发送数据,进一步的数据路由可以是任意的复杂
比如:将数据广播到多个目标:
@coroutine
def broadcast(targets):
while True:
item = (yield)
for target in targets:
target.send(item)
相对一般的迭代器而言,协程提供了更加强大的数据路由可能性
如果你构建了一堆数据处理组件,你可以通过一系列复杂的管道,
分支,合并等操作将它们粘合起来
不过协程也有一些限制(放在后边谈)
另外,yield并不是表达式(?)
想把它当作C语言一样来写的同学要悲剧了:
while(line=yield)是语法错误。。。
协程和对象:
协程在某些程度上和OO设计模式的
请求处理器差不多
class GrepHandler(object):
def __init__(self,pattern, target):
self.pattern = pattern
self.target = target
def send(self,line):
if self.pattern in line:
self.target.send(line)
协程版本的:
@coroutine
def grep(pattern,target):
while True:
line = (yield) # Receive a line
if pattern in line:
target.send(line)
协程比类要简单且快(50%左右)
性能差距在于self指针的使用
第三部分:
协程与事件分派
协程可以用来写很多处理事件流的组件
现在展示一个处理xml组件:
一个记录当前巴士的xml:
<?xml version="1.0"?>
<buses>
<bus>
<id>7574</id>
<route>147</route>
<color>#3300ff</color>
<revenue>true</revenue>
<direction>North Bound</direction>
<latitude>41.925682067871094</latitude>
<longitude>-87.63092803955078</longitude> <pattern>2499</pattern>
<patternDirection>North Bound</patternDirection>
<run>P675</run>
<finalStop><[CDATA[Paulina & Howard Terminal]]></finalStop> <operator>42493</operator>
</bus>
</buses>
使用SAX库,一个事件驱动的xml解析库
SAX主要是在处理大xml文件时,使用增量的处理而不用占用大量内存
但事件驱动 的特性也使得SAX在处理时相当低级与笨拙
可以像这样的分派事件到协程:
class EventHandler(xml.sax.ContentHandler):
def __init__(self,target):
self.target = target
def startElement(self,name,attrs):
self.target.send(('start',(name,attrs._attrs)))
def characters(self,text):
self.target.send(('text',text))
def endElement(self,name):
self.target.send(('end',name))
随后 ,作者又展示了一个使用expat的样例性能提升83%
使用 c语言写的扩展,又提升55%
xml.etree.cElementTree是快速xml解析,实验表明,协程和它性能不相上下
小结:协程与生成器相似
你可以创建一堆处理组件并连接它们到一起
可以通过创建管道,数据流图等 方法处理数据
也可以使用协程来完成相当有技巧的执行(比如,事件驱动的系统)
并行:
你发送数据给协程
也可以发送数据给线程(通过队列)
也可以发送数据给进程(通过消息)
协程自然就与线程和分布式系统问题联系在一起了。
可以通过在线程内部或进程内部添加一个额外的层打包协程
示例在这里:
# cothread.py
#
# A thread object that runs a coroutine inside it. Messages get sent
# via a queue object
from threading import Thread
import queue
from coroutine import *
@coroutine
def threaded(target):#这是用来将数据推送到下一个协程的代码(处于新的线程里)
messages = queue.Queue()
def run_target():
while True:
item = messages.get()
if item is GeneratorExit:
target.close()
return
else:
target.send(item)
Thread(target=run_target).start()#为后纯
try:#主线程协程的代码
while True:
item = (yield)
messages.put(item)
except GeneratorExit:
messages.put(GeneratorExit)
# Example use
if __name__ == '__main__':
import xml.sax
from cosax import EventHandler
from buses import *
xml.sax.parse("/Users/ly/ws/allroutes.xml", EventHandler(
buses_to_dicts(
threaded(
filter_on_field("route","22",
filter_on_field("direction","North Bound",
bus_locations()))
))))
整个图的关系可能看起来有些奇怪
添加线程会使性能降低50%左右。
也可以使用多进程的管道将两个协程
# coprocess.py
#
# An example of running a coroutine in a subprocess connected by a pipe
import pickle
from coroutine import *
@coroutine
def sendto(f):
try:
while True:
item = (yield)
pickle.dump(item,f)
f.flush()
except StopIteration:
f.close()
def recvfrom(f,target):
try:
while True:
item = pickle.load(f)
target.send(item)
except EOFError:
target.close()
# Example use
if __name__ == '__main__':
import xml.sax
#from cosax import EventHandler
from buses import *
import subprocess
p = subprocess.Popen(['python','busproc.py'],
stdin=subprocess.PIPE)
xml.sax.parse("allroutes.xml",
EventHandler(
buses_to_dicts(
sendto(p.stdin))))
(貌似pickle包有问题,上边的代码不能通过)
需要注意的是,你必须有足够的其他东西补偿多进程通讯造成的损失。
(比如,足够的多CPU和进程)
你可以使用协程将任务的实现从它的执行环境分隔开来
协程就是实现
而环境是你选择任意(线程,多进程,网络等 )
警告
使用大量的协程,线程,及进程很容易造成一个不可维护的程序。
也有可能使程序缓慢
需要仔细地研究以知道这些是不是一个好主意
一些隐藏的危险:
对一个协程的send调用必须恰当地同步
如果你对一个正在正在执行中的协程调用send ,你的程序会当掉
比如:多线程对同一个目标发送数据
限制:
你不能创建一个协程循环或环(即yield 对自己调用send)
堆栈化的send组成了一种调用栈
如果你调用一个正在协程推送中的进程,会得到一个错误
send并不挂起协程的执行