更多关于回调的知识html
稍微停下来再思考一下回调的机制。尽管对于以Twisted方式使用Deferred写一个简单的异步程序已经很是了解了,但Deferred提供更多的是只有在比较复杂环境下才会用到的功能。所以,下面咱们本身想出一些复杂的环境,以此来观察当使用回调编程时会遇到哪些问题。而后,再来看看deferred是如何解决这些问题的。python
所以,咱们为诗歌下载客户端添加了一个假想的功能。设想一些计算机科学家发明了一种新诗歌关联算法,react
Byronification引擎。这个漂亮的算法根据一首诗歌生成一首使用Lord Byron式的一样的诗歌。另外,专家们提供了其Python的接口,即:git
class IByronificationEngine(Interface): def byronificate(poem): """ Return a new poem like the original, but in the style of Lord Byron. Raises GibberishError if the input is not a genuine poem. """
像大多数高尖端的软件同样,其实现都存在着许多bugs。这意外着除了已知的异常外,这个byronificate 方法可能会抛出一些专家当时没有预料到的异常出来。github
咱们还能够假设这个引擎可以很是快的动做以致于咱们能够在主线程中调用到而无需考虑使用reactor。下面是咱们想让程序实现的效果:算法
1.尝试下载诗歌编程
2.若是下载失败,告诉用户没有获得诗歌网络
3.若是下载到诗歌,则转交给Byronificate处理引擎一份架构
4.若是引擎抛出GibberishError,告诉用户没有获得诗歌app
5.若是引擎抛出其它异常,则将原始式样的诗歌立给用户
6.若是咱们获得这首诗歌,则打印它
7.结束程序
这里设计是当遇到GibberishError异常则表示没有获得诗歌,所以咱们直接告诉用户下载失败便可。这也许对调试没什么用处,但咱们的用户关心的只是咱们下载到诗歌没有。另外一方面,若是引擎由于一些其它的缘由而出现处理失败,那么咱们将原始诗歌交给用户。毕竟,有诗歌呈现总比没有好,虽然不是用户想要的Byron样式。
下面是同步模式的代码:
try: poem = get_poetry(host, port) # synchronous get_poetry except: print >>sys.stderr, 'The poem download failed.' else: try: poem = engine.byronificate(poem) except GibberishError: print >>sys.stderr, 'The poem download failed.' except: print poem # handle other exceptions by using the original poem else: print poem sys.exit()
这段代码可能通过一些重构会更加简单,但已经足以说明上面的逻辑流程。咱们想升级那些最近使用deferred的客户端来使用这个功能。但这部份内容我准备把它放在第十部分。如今,咱们来考虑一下,用版本3.1来实现这个功能,最后一个没有使用deferred的客户端。假设咱们无需考虑处理异常,那么只是改变一下got_poem回调便可:
def got_poem(poem): poems.append(byron_engine.byronificate(poem)) poem_done()
那么若是byronificate抛出GibberishError异常或其它异常会发生什么呢?看看第六部分的图11,咱们能够获得:
1.这个异常会传播到工厂中的poem_finished回调,即激活got_poem的方法
2.因为poem_finished并无捕获这个异常,所以其会传递到protocol中的poemReceive函数
3.而后来到connectionLost函数,仍然在protocol中
4.而后就来到Twisted的核心区,最后止步于reactor。
前面已经了解到,reactor会捕获异常并记录它而不是“崩溃”掉。但它却不会告诉用户咱们的诗歌下载失败的消息。reactor并不知道任何诗歌或GibberishError
s的信息,它只是一段被设计成适应全部网络类型的通用代码,即使与诗歌无关的网络服务。(Dave这里想强调的是reactor只是作一些具备广泛意义的事情,不会单独去处理特定的问题,例如这里原GibberishError
s异常)
注意异常是如何顺着调用链传递到具备通用性代码区域。而且看到,在got_poem后面任何一步都没有可望以咱们客户端的具体要求来处理异常的。这与同步代码中的方式偏偏相反。
图15揭示了一个同步客户端的调用栈:
图15:同步调用栈
main函数是最高层,意味着它能够触及整个程序,它为何要存在,而且它是如何在总体上表现的。典型的,main函数能够触及到用户在命令行输入想让程序作什么的参数。而且它还有一个特殊的目的:为一个命令行式的客户端打印结果。
socket的connet函数,偏偏相反,其为最低层。它所知道的就是提供到指定地址的链接。它并不知道另外一端是什么及咱们为何要进行链接。但connect有通用性,无论你由于何种服务要进行网络链接均可以使用它。
get_poetry在中间,它知道要取一些诗歌,但并不知道若是得不到诗歌会发生什么。所以,从connect抛出的异常会向上传递,从低层的具备通用性的代码区到高层的具备针对性的代码区,直到其传递到知道如何处理这个异常的代码区。
如今,咱们再回来看看对3.1版的假想功能的实现。咱们在图16里对调用栈进行了分析,固然只是说明了其中关键的函数:
图16 异步调用栈
如今问题很是清晰了:在回调中,低层的代码(reactor)调用高层的代码,其甚至还会调用更高层的代码。所以一旦出现了异常,它并不会当即被其附件(在调用栈中可触及)的代码捕获,固然附近的代码也不可能处理它。因为异常每向上传递一次,就越靠近低层那些更加不知如何处理该异常的代码。
一旦异常来到Twisted的核心代码区,游戏也就结束了。异常并不会被处理,只是被记录下来。所以咱们在以最原始的回调方式使用回调时(不使用deferred),必须在其进入Twisted之间很好地处理各类异常,至少是咱们知道的那些在咱们本身设定的规则下会产生的异常。固然其也应该包括那些由咱们本身的BUG产生的异常。
因为bug可能存在于咱们代码中的每一个角落,所以咱们必须将每一个回调都放入try/except中,这样一来全部的异常都才有可能被捕获。这对于咱们的errback一样适用,由于errback中也可能含有bugs。
Deferred的优秀架构
最终还得由Deferred来帮咱们解决这类问题。当一个deferred激活了一个callback或errback时,它就会捕获各类由回调抛出的异常。换句话说,deferred扮演了try/except模块,这样一来,只要咱们使用deferred就无需本身来实现这一层了。那deferred是如何解决这个问题的?很简单,它传递异常给在其链上的下一个errback。
咱们添加到deferred中的第一个errback回调来处理任何出错信息,信息是在deferred的errback函数调用时发出的。但第二个errback会处理任何由第一个errback或第一个callback抛出的异常,并一直按这种规则传递下去。
回忆下图12.咱们假设第一对callback/errback是stage0,下面则是stage1,stage2。。。依次类推。
对于stage N来讲,若是其callback或errback出错,那么stage N+1的errback就会被调用并收到一个Failure对象做为参数,同时stage N+1的callback就不会被调用了。
经过将回调函数产生的异常向在链中传递,deferred将异常抛向了高层代码。这也意味着调用deferred的callback与errback永远不会在调用都自己处引起异常(只要你仅激活deferred一次),所以,底层的代码能够放心的激活deferred而无需担忧会引起异常。相反,高层代码经过向deferred中添加errback(使用addErrback)来捕获异常。
在同步代码中,异常会在其被捕获而中止传递,那么一个errback如何发出其捕获了异常这一信号呢?一样很简单:再也不引起异常。这样一来,执行权就转移到了callback中来。所以对于stage N来讲,不论是callback仍是errback成功执行而没有抛出异常,那么stage N+1的callback就会被调用,一样,stage N+1的errback就不会被调用了。
咱们来总结一下吧:
1.一个deferred有一个callback/errback对链,它们以添加到deferred中的顺序依次排列
2.stage 0,即第一对errback/callbac,会在deferred激活时调用,具体调用那个看激活deferred的方式,如果经过.errback激活,则调用errback;一样如果经过.callback激活则调用callback。(这里的errback/callback实际是指经过addBoth添加的函数)
3.若是stage N执行出现异常,则stage N+1的errback被调用,而且其参数即为stage N出现的异常
4.一样,若是stage N成功,即没有抛出异常,则N+1的callback被调用,其第一个参数为stage N的返回值。
图17更加直观的描述上述操做:
图17:deferred中的控制流程
绿色的线表示callback和errback成功执行没抛出异常,而红线表示出现了异常。这些线不只说明了控制流程还说明了异常与返回值在链中流动的状况。图17显示了全部deferred能出现的可能路径,但实际只有一条路径会存在。图18显示了一条可能的路径:
图18:可能的deferred激活路线
图18中,deferred的.callback函数被调用了,所以激活了stage 0的callback。这个callback成功的执行而没有抛出异常,所以控制权传给了stage 1的callback。但这个callbac执行失败而抛出异常,所以控制权传给了stage 2的errback。errback成功的处理了异常,而没有再抛出异常,所以控制权传给了stage 3的callback,而且将errback的返回值做为第一个参数传了进来(即stage 3的callback中)。
图18中,能够看出,最后一个stage上的全部的回调出现异常时,都由下一层的errback来捕获并处理,但若是最后一个stage的callback或errback执行失败而抛出异常,怎么办呢?那么这个异常就会成为unhandled(未处理)。
在同步代码中,未处理的异常会致使解释器崩溃,在原始方式使用回调的代码中未处理异常会由reactor捕获并记录下来。那么未处理异常出如今deferred中会怎样呢?让咱们来作个试验。运行twisted-deferred/defer-unhandled.py试试。下面是输出:
Finished Unhandled error in Deferred: Traceback (most recent call last): ... --- <exception caught here> --- ... exceptions.Exception: oops
以下几点须要引发咱们的注意:
1.最后一个print函数成功执行,意味着程序并无由于出现未处理异常而崩溃。
2.其只是将跟踪栈打印出来,而没有宕掉解释器
3.跟踪栈的内容告诉咱们deferred在何处捕获了异常
4.“’Unhandle”的字符在“Finished”以后出现。
之因此出现第4条是由于,这个消息只有在deferred被垃圾回收时才会打印出来。咱们将在下面的部分看到其中的缘由。
在同步代码中,咱们可使用raise来从新抛出一个异常而无需其它参数。一样,咱们也能够在errback中这样作。deferred经过如下两点来判断callback/errback是否执行成功:
1.callback/errback “raise”一个异常,或
2.callbakc/errback返回一个Failure对象
由于errback的第一个参数就是一个Failure,所以一个errback能够在进行完其处理后能够再次抛出这个Failure。
Callbacks与Errbacks,成对出现
上面讨论内容中的一个问题必需要清楚:你添加callback与errback到一个defered的顺序会决定这个deferred的的总体运行状况。另外一个必须搞清楚的是:在一个deferred中callback与errback每每是成对出现。有四个方法能够向一个deferred的回调链中添加callback/errback对:
一、addCallbacks
二、addCallback
三、addErrback
四、addBoth
很明显的是,第一个与第四个是向链中添加函数对。固然中间
两个也向链中添加函数对。
AddCallback
向链中添加一个显式的
callback
函数与一个隐式的”
pass-through“
函数(实在想不出一个对应的词)。一个
pass-through
函数只是虚设的函数,只将其第一个参数返回。因为
errback
回调函数的第一个参数是
Failure
,所以一个“
path-through”
的
errback
老是执行“失败”,即将异常传给下个
errback
回调。
deferred
模拟器
这部份内容,没有译。其主要是帮助理解deferred,但你会发现,读其中的代码,根本更好的理解deferred。主要是我尚未理解,嘿嘿。因此就不知为不知吧。
总结
通过这些对回调的考虑,发现因为回调式编程改变了低层代码与高层代码的关系,所以让回调产生的异常直接抛到栈中并不件好事。Deferred经过将异常捕获而后将其顺着回调链传递来解决了这个问题。
咱们一样意识到,原始数据(返回值)在链中被传递。结合这个两事实也就带来了这样一种场景:根据每一个stage收到的结果的不一样,deferred在callback与errback链中来回交错传递数据并执行。
咱们将在第十部分使用些学到的知识来更新咱们的客户端。