这部分咱们将回到"回调"这个主题.咱们将介绍另一种写回调函数的方法,即在Twisted中使用 generators. 咱们将演示如何使用这种方法而且与使用"纯" Deferreds
进行对比. 最后, 咱们将使用这种技术重写诗歌客户端. 但首先咱们来回顾一下generators
的工做原理,以便弄清楚它为什么是建立回调的候选方法.html
你可能知道, 一个Python生成器是一个"可重启的函数",它是在函数体中用 yield
语句建立的. 这样作可使这个函数变成一个"生成器函数",它返回一个"iterator"能够用来以一系列步骤运行这个函数. 每一个迭代循环都会重启这个函数,继续执行到下一个yield
语句.python
生成器(和迭代器)一般被用来表明以惰性方式建立的值序列. 看一下如下文件中的代码 inline-callbacks/gen-1.py:react
def my_generator(): print 'starting up' yield 1 print "workin'" yield 2 print "still workin'" yield 3 print 'done' for n in my_generator(): print n
这里咱们用生成器建立了1,2,3序列. 若是你运行这些代码,会看到在生成器上作迭代时,生成器中的 print
与循环语句中的print
语句交错出现.git
如下自定义迭代器代码使上面的说法更加明显(inline-callbacks/gen-2.py):github
def my_generator(): print 'starting up' yield 1 print "workin'" yield 2 print "still workin'" yield 3 print 'done' gen = my_generator() while True: try: n = gen.next() except StopIteration: break else: print n
视做一个序列,生成器仅仅是获取连续值的一个对象.但咱们也能够以生成器自己的角度看问题:编程
生成器函数在被循环调用以前并无执行(使用 next
方法).服务器
一旦生成器开始运行,它将一直执行直到返回"循环"(使用 yield
)app
当循环中运行其余代码时(如 print
语句),生成器则没有运行.异步
当生成器运行时, 则循环没有运行(等待生成器返回前它被"阻滞"了).ide
一旦生成器将控制交还到循环,再启动须要等待任意可能时间(其间任意量的代码可能被执行).
这与异步系统中的回调工做方式很是相似. 咱们能够把 while
循环视做 reactor
, 把生成器视做一系列由 yield
语句分隔的回调函数. 有趣的是, 全部的回调分享相同的局部变量名空间, 并且名空间在不一样回调中保持一致.
进一步,你能够一次激活多个生成器(参考例子 inline-callbacks/gen-3.py),使得它们的"回调"互相交错,就像在Twisted系统中独立运行的异步程序.
然而,这种方法仍是有一些欠缺.回调不只仅被 reactor
调用, 它还能接受信息.做为 deferred
链的一部分,回调要么接收Python值形式的一个结果,要么接收 Failure
形式的一个错误.
从Python2.5开始,生成器功能被扩展了.当你再次启动生成器时,能够给它发送信息,如 inline-callbacks/gen-4.py 所示:
class Malfunction(Exception): pass def my_generator(): print 'starting up' val = yield 1 print 'got:', val val = yield 2 print 'got:', val try: yield 3 except Malfunction: print 'malfunction!' yield 4 print 'done' gen = my_generator() print gen.next() # start the generator print gen.send(10) # send the value 10 print gen.send(20) # send the value 20 print gen.throw(Malfunction()) # raise an exception inside the generator try: gen.next() except StopIteration: pass
在Python2.5之后的版本中, yield
语句是一个计算值的表达式.从新启动生成器的代码可使用 send
方法代替 next
决定它的值(若是使用 next
则值为 None), 并且你还能够在迭代器内部使用 throw
方法抛出任何异常. 是否是很酷?
根据咱们刚刚回顾的能够向生成器发送值或抛出异常的特性,能够设想它是像 deferred
中的一系列回调,便可以接收结果或错误. 每一个回调被 yield
分隔,每个 yield
表达式的值是下一个回调的结果(或者 yield
抛出异常表示错误).图35显示相应概念:
图35:做为回调序列的生成器
如今一系列回调以 deferred
方式被连接在一块儿,每一个回调从它前面的回调接收结果.生成器很容易作到这一点——当再次启动生成器时,仅仅使用 send
发送上一次调用生成器的结果( yield
产生的值).但这看起来有点笨,既然生成器从开始就计算这个值,为何还须要把它发送回来? 生成器能够将这个值储存在一个变量中供下一次使用. 所以这究竟是为何呢?
回忆一下咱们在第十三部分 中所学, deferred
中的回调还能够返回 deferred
自己. 在这种状况下, 外层的 deferred
先暂停等待内层的 deferred
激发,接下来外层 deferred
链使用内层 deferred
的返回结果(或错误)激发后续的回调(或错误回调).
因此设想咱们的生成器生成一个 deferred
对象而不是一个普通的Python值. 这时生成器会自动"暂停";生成器老是在每一个yield
语句后暂停直到被显示的重启.于是咱们能够延迟它的重启直到 deferred
被激发, 届时咱们会使用 send
方法发送值(若是 deferred
成功)或者抛出异常(若是 deferred
失败).这就使咱们的生成器成为一个真正的异步回调序列,这正是twisted.internet.defer 中 inlineCallbacks 函数背后的概念.
考虑如下例程, 位于 inline-callbacks/inline-callbacks-1.py:
from twisted.internet.defer import inlineCallbacks, Deferred @inlineCallbacks def my_callbacks(): from twisted.internet import reactor print 'first callback' result = yield 1 # yielded values that aren't deferred come right back print 'second callback got', result d = Deferred() reactor.callLater(5, d.callback, 2) result = yield d # yielded deferreds will pause the generator print 'third callback got', result # the result of the deferred d = Deferred() reactor.callLater(5, d.errback, Exception(3)) try: yield d except Exception, e: result = e print 'fourth callback got', repr(result) # the exception from the deferred reactor.stop() from twisted.internet import reactor reactor.callWhenRunning(my_callbacks) reactor.run()
运行这个例子能够看到生成器运行到最后并终止了 reactor
, 这个例子展现了 inlineCallbacks
函数的不少方面.首先,inlineCallbacks
是一个修饰符,它老是修饰生成器函数,如那些使用 yield
语句的函数. inlineCallbacks
的所有目的是将一个生成器按照上述策略转化为一系列异步回调.
第二,当咱们调用一个用 inlineCallbacks
修饰的函数时,不须要本身调用 send
或 throw
方法.修饰符会帮助咱们处理细节,并确保生成器运行到结束(假设它不抛出异常).
第三,若是咱们从生成器生成一个非延迟值,它将以 yield
生成的值当即重启.
最后,若是咱们从生成器生成一个 deferred
,它不会重启除非此 deferred
被激发.若是 deferred
成功返回,则 yield
的结果就是 deferred
的结果.若是 deferred
失败了,则 yield
会抛出异常. 注这个异常仅仅是一个普通的 Exception
对象,而不是Failure
,咱们能够在 yield
外面用 try/except
块捕获它们.
在上面的例子中,咱们仅用 callLater
在一小段时间以后去激发 deferred
.虽然这是一种将非阻塞延迟放入回调链的实用方法,但一般咱们会生成一个 deferred
,它是被生成器中其余的异步操做(如 get_poetry)返回的.
OK,如今咱们知道了 inlineCallbacks
修饰的函数是如何运行的,但当你实际调用时会返回什么值呢?正如你认为的,将获得deferred
.因为不能确切地知道生成器什么时候中止(它可能生成一个或多个 deferred
),装饰函数自己是异步的,因此 deferred
是一个合适的返回值.注:这个返回的 deferred
不是生成器中 yield
生成的 deferred
.相反,它在生成器彻底结束(或抛出异常)后才被激发.
若是生成器抛出一个异常,那么返回的 deferred
将激发它的错误回调链,把异常包含在一个 Failure
中. 可是若是咱们但愿生成器返回一个正常值,必须使用 defer.returnValue
函数. 就像普通 return
语句同样,它也会终止生成器(实际会抛出一个特殊异常).例子 inline-callbacks/inline-callbacks-2.py 说明了这两种可能.
让咱们在新版本的诗歌客户端中加入 inlineCallbacks
,你能够在 twisted-client-7/get-poetry.py 中查看源代码.也许你须要与客户端6.0—— twisted-client-6/get-poetry.py 进行对比,它们的相对变化位于 poetry_main:
def poetry_main(): addresses = parse_args() xform_addr = addresses.pop(0) proxy = TransformProxy(*xform_addr) from twisted.internet import reactor results = [] @defer.inlineCallbacks def get_transformed_poem(host, port): try: poem = yield get_poetry(host, port) except Exception, e: print >>sys.stderr, 'The poem download failed:', e raise try: poem = yield proxy.xform('cummingsify', poem) except Exception: print >>sys.stderr, 'Cummingsify failed!' defer.returnValue(poem) def got_poem(poem): print poem def poem_done(_): results.append(_) if len(results) == len(addresses): reactor.stop() for address in addresses: host, port = address d = get_transformed_poem(host, port) d.addCallbacks(got_poem) d.addBoth(poem_done) reactor.run()
在这个新版本里, inlineCallbacks
生成函数 get_transformed_poem 负责取回诗歌而且应用变换(经过变换服务).因为这两个操做都是异步的,咱们每次生成一个 deferred
而且隐式地等待结果.与客户端6.0同样,若是变换失败则返回原始诗歌.咱们可使用 try/except
语句捕获生成器中的异步错误.
咱们以先前的方式测试新版客户端. 首先启动一个变换服务:
python twisted-server-1/tranformedpoetry.py --port 10001
而后启动两个诗歌服务器:
python twisted-server-1/fastpoetry.py --port 10002 poetry/fascination.txt python twisted-server-1/fastpoetry.py --port 10003 poetry/science.txt
如今能够运行新的客户端:
python twisted-client-7/get-poetry.py 10001 10002 10003
试试关闭一个或多个服务器,看一看客户端如何捕获错误.
就像 Deferred
对象, inlineCallbacks
函数给咱们一种组织异步回调的新方式.同时,如同使用 deferred
,inllineCallbacks
没有改变游戏规则.特别地,咱们的回调仍然一次调用一个回调,它们仍然被 reactor
调用.咱们能够经过打印内联回调的回溯跟踪信息来证明这一点,参见脚本 inline-callbacks/inline-callbacks-tb.py.运行此代码你将首先得到一个关于 reactor.run() 的回溯,而后是许多帮助函数信息,最后是咱们的回调.
图29解释了当 deferred
中一个回调返回另外一个 deferred
时会发生什么,咱们调整它来展现当一个 inlineCallbacks
生成器生成一个 deferred
时会发生什么,参考图36:
图36: inlineCallbacks
函数中的控制流
一样的图对两种状况都适用,由于它们表示的想法都是同样的 —— 一个异步操做正在等待另外一个.
因为 inlineCallbacks
和 deferred
解决许多相同的问题,在它们之间如何选择呢?下面列出一些 inlineCallbacks
的潜在优点.
因为回调分享相同的名空间,所以没有必要传递额外状态.
回调的顺序很容易看到,由于它老是从上到下执行.
节省了每一个回调函数的声明和隐式控制流,一般减小输入.
可使用熟悉的 try/except 语句处理错误.
固然也存在一些缺陷:
生成器中的回调不能被单独调用,这使代码重用比较困难.而构造 deferred
的代码则可以以任意顺序自由地添加任何回调.
生成器的紧致性可能混淆一个事实,其实异步回调很是晦涩.尽管生成器看起来像一个普通的函数序列,可是它的行为却很是不同. inlineCallbacks
函数不是一种避免学习异步编程模型的方式.
就像任何技术,实践将积累出必要的经验,帮你作出明智选择.
在这个部分,咱们学习了 inlineCallbacks
装饰符以及它怎样使咱们可以以Python生成器的形式表达一系列异步回调.
在 第十八部分 中,咱们将学习一种管理 一组 "并行"异步操做的技术.
参考练习
为何 inlineCallbacks
函数是复数(形式)?
研究 inlineCallbacks 的实现以及它们帮助函数 _inlineCallbacks. 并思考短语"魔鬼在细节处".
有N个 yield
语句的生成器中包含多少个回调,假设其中没有循环或者 if
语句?
诗歌客户端7.0可能同时运行三个生成器.概念上,它们之间有多少种不一样的交错方式?考虑诗歌客户端和 inlineCallbacks
的实现,你认为实际有多少种可能?
把客户端7.0中的 got_poem 放入到生成器中.
把 poem_done 回调放入生成器.当心!确保处理全部失败状况以便不管怎样 reactor
都会关闭.与使用 deferred
关闭reactor
对比代码有何不一样?
一个在 while
循环中使用 yield
语句的生成器表明一个概念上的无限序列.那么一样的装饰有 inlineCallbacks
的生成器又表明什么呢?