Python Twisted 学习系列9(转载stulife最棒的Twisted入门教程)

第九部分:第二个小插曲,Deferred

作者:[email protected]http://krondo.com/?p=1825译者:杨晓伟(采用意译)

第九部分:第二个小插曲,Deferred

可以从这里从头来阅读这个系列

更多关于回调的知识

稍微停下来再思考一下回调的机制。尽管对于以Twisted方式使用Deferred写一个简单的异步程序已经非常了解了,但Deferred提供更多的是只有在比较复杂环境下才会用到的功能。因此,下面我们自己想出一些复杂的环境,以此来观察当使用回调编程时会遇到哪些问题。然后,再来看看deferred是如何解决这些问题的。

因此,我们为诗歌下载客户端添加了一个假想的功能。设想一些计算机科学家发明了一种新诗歌关联算法,

Byronification引擎。这个漂亮的算法根据一首诗歌生成一首使用Lord Byron式的同样的诗歌。另外,专家们提供了其Python的接口,即:

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 方法可能会抛出一些专家当时没有预料到的异常出来。

我们还可以假设这个引擎能够非常快的动作以至于我们可以在主线程中调用到而无需考虑使用reactor。下面是我们想让程序实现的效果:

1.尝试下载诗歌

2.如果下载失败,告诉用户没有得到诗歌

3.如果下载到诗歌,则转交给Byronificate处理引擎一份

4.如果引擎抛出GibberishError,告诉用户没有得到诗歌

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并不知道任何诗歌或GibberishErrors的信息,它只是一段被设计成适应所有网络类型的通用代码,即便与诗歌无关的网络服务。(Dave这里想强调的是reactor只是做一些具有普遍意义的事情,不会单独去处理特定的问题,例如这里原GibberishErrors异常)

注意异常是如何顺着调用链传递到具有通用性代码区域。并且看到,在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

CallbacksErrbacks,成对出现

上面讨论内容中的一个问题必须要清楚:你添加callback与errback到一个defered的顺序会决定这个deferred的的整体运行情况。另一个必须搞清楚的是:在一个deferred中callback与errback往往是成对出现。有四个方法可以向一个deferred的回调链中添加callback/errback对:

  1. addCallbacks
  2. addCallback
  3. addErrback
  4. addBoth

很明显的是,第一个与第四个是向链中添加函数对。当然中间两个也向链中添加函数对。AddCallback向链中添加一个显式的callback函数与一个隐式的”pass-through“函数(实在想不出一个对应的词)。一个pass-through函数只是虚设的函数,只将其第一个参数返回。由于errback回调函数的第一个参数是Failure,因此一个“path-through”errback总是执行“失败”,即将异常传给下个errback回调。

deferred模拟器

这部分内容,没有译。其主要是帮助理解deferred,但你会发现,读其中的代码,根本更好的理解deferred。主要是我还没有理解,嘿嘿。所以就不知为不知吧。

总结

经过这些对回调的考虑,发现由于回调式编程改变了低层代码与高层代码的关系,因此让回调产生的异常直接抛到栈中并不件好事。Deferred通过将异常捕获然后将其顺着回调链传递来解决了这个问题。

我们同样意识到,原始数据(返回值)在链中被传递。结合这个两事实也就带来了这样一种场景:根据每个stage收到的结果的不同,deferred在callback与errback链中来回交错传递数据并执行。

我们将在第十部分使用些学到的知识来更新我们的客户端。

时间: 2024-10-08 10:25:07

Python Twisted 学习系列9(转载stulife最棒的Twisted入门教程)的相关文章

Python Twisted 学习系列3(转载stulife最棒的Twisted入门教程)

第三部分:初步认识Twisted 作者:[email protected]http://krondo.com/?p=1333译者:杨晓伟(采用意译) 第三部分:开始认识Twisted 可以从这里从头开始阅读这个系列. 用twisted的方式实现前面的内容 最终我们将使用twisted的方式来重新实现我们前面的异步模式客户端.不过,首先我们先稍微写点简单的twisted程序来认识一下twisted. 最最简单的twisted程序就是下面的代码,其在twisted-intro目录中的basic-tw

Python Twisted 学习系列1(转载stulife最棒的Twisted入门教程)

第一部分:Twisted理论基础 作者:[email protected]http://krondo.com/?p=1209译者:杨晓伟(采用意译) 前言: 最近有人在Twisted邮件列表中提出诸如”为任务紧急的人提供一份Twisted介绍”的的需求.值得提前透露的是,这个序列并不会如他们所愿.尤其是介绍Twisted框架和基于Python 的异步编程而言,可能短时间无法讲清楚.因此,如果你时间紧急,这恐怕不是你想找的资料. 我相信如果对异步编程模型一无所知,快速的介绍同样无法让你对其有所理解

Python Twisted 学习系列4(转载stulife最棒的Twisted入门教程)

第四部分:由Twisted支持的诗歌客户端 作者:[email protected]://krondo.com/?p=1445译者:杨晓伟(采用意译) 第四部分:由Twisted支持的诗歌客户端 你可以在这里从头开始阅读这个系列. 第一个twisted支持的诗歌服务器 尽管Twisted大多数情况下用来写服务器代码,为了一开始尽量从简单处着手,我们首先从简单的客户端讲起. 让我们来试试使用Twisted的客户端.源码在twisted-client-1/get-poetry.py.首先像前面一样要

Python Twisted 学习系列20(转载stulife最棒的Twisted入门教程)

第二十部分 轮子中的轮子: Twisted和Erlang 简介 在这个系列中,有一个事实我们还没有介绍,即混合同步的"普通Python"代码与异步Twisted代码不是一个简单的任务,因为在Twisted程序中阻滞不定时间将使异步模型的优势丧失殆尽. 如果你是初次接触异步编程,那么你得到的知识看起来有一些局限.你可以在Twisted框架内使用这些新技术,而不是在更广阔的一般Python代 码世界中.同时,当用Twisted工作时,你仅仅局限于那些专门为作为Twisted程序一部分所写的

Python Twisted 学习系列19(转载stulife最棒的Twisted入门教程)

第十九部分 改变之前的想法 简介 Twisted是一个正在进展的项目,它的开发者会定期添加新的特性并且扩展旧的特性. 随着Twisted 10.1.0发布,开发者向 Deferred 类添加了一个新的特性—— cancellation ——这正是我们今天要研究的. 异步编程将请求和响应解耦了,如此又带来一个新的可能性:在请求结果和返回结果之间,你可能决定不再需要这个结果了.考虑一下 :doc:`p14` 中的诗歌代理服务器.下面是这个如何工作的,至少对于诗歌的第一次请求: 一个对诗歌的请求来了.

Python Twisted 学习系列11(转载stulife最棒的Twisted入门教程)

第十一部分:改进诗歌下载服务器 作者:[email protected]http://krondo.com/?p=2048译者:杨晓伟(采用意译) 第十一部分:改进诗歌下载服务器 你可以从这里从头阅读这个系列. 诗歌下载服务器 到目前为止,我们已经学习了大量关于诗歌下载客户端的Twisted的知识,接下来,我们使用Twisted重新实现我们的服务器端.利益于Twisted的抽象机制,接下来你会发现我们前面已经几乎全部学习到这部分知识了.其实现源码在twisted-server-1/fastpoe

Python Twisted 学习系列22(转载stulife最棒的Twisted入门教程)

第二十二部分 结束 全部完成 呼呼! 感谢你一路支持. 在我开始时完全没有想到这个系列会这样长,会花这么多时间完成,但是创建这个系列的过程使我非常享受,也希望你喜欢它. 既然我已经完成了,我会进一步考虑将其转化为PDF格式.然而,不保证. 最后,我想总结一些帮助你继续学习Twisted的建议. 进一步阅读 首先,我建议阅读Twisted的 在线文档. 虽然它备受指责,但我觉得这总比饱受赞誉要好. 如果你希望使用Twisted进行网络编程, 那么 Jean-Paul Calderone 的广受关注

Python Twisted 学习系列5(转载stulife最棒的Twisted入门教程)

第五部分:由Twisted支持的诗歌客户端 作者:[email protected] http://krondo.com/?p=1522译者:杨晓伟(采用意译) 第五部分:由Twited支持的诗歌下载服务客户端 你可以从这里从头开始阅读这个系列 抽象地构建客户端 在第四部分中,我们构建了第一个使用Twisted的客户端.它确实能很好地工作,但仍有提高的空间. 首先是,这个客户端竟然有创建网络端口并接收端口处的数据这样枯燥的代码.Twisted理应为我们实现这些例程性功能,省得我们每次写一个新的程

Python Twisted 学习系列6(转载stulife最棒的Twisted入门教程)

第六部分:抽象地利用Twisted 作者:[email protected]http://krondo.com/?p=1595译者:杨晓伟(采用意译) 第六部分:抽象地利用Twisted 你可以从这里从头开始阅读这个系列. 打造可以复用的诗歌下载客户端 我们在实现客户端上已经花了大量的工作.最新版本的(2.0)客户端使用了Transports,Protocols和Protocol Factories,即整个Twisted的网络框架.但仍有大的改进空间.2.0版本的客户端只能在命令行里下载诗歌.这

Python Twisted 学习系列8(转载stulife最棒的Twisted入门教程)

第八部分:使用Deferred的诗歌下载客户端 作者:[email protected]http://krondo.com/?p=1778译者:杨晓伟(采用意译) 第八部分:使用Deferred的诗歌下载客户端 可以从这里从头开始阅读这个系列. 客户端4.0 我们已经对deferreds有些理解了,现在我们可以使用它重写我们的客户端.你可以在twisted-client-4/get-poetry.py中看到它的实现. 这里的get_poetry已经再也不需要callback与errback参数了