本文的目的是介绍Deferred,Twisted的控制异步代码流的机制,不要求你有Twisted的相关知识,只需知道知道Python语法即可。
写Python代码时,一个很流行的常识就是同一个代码块中某行代码,只有在它前面的一行代码执行完毕之后才会轮到它执行:
pod_bay_doors.open()
pod.launch()
只有分离舱门打开,然后吊舱才能发射。一行接着一行地执行是Python内建的执行代码的机制,这样执行代码清楚、简洁而且没有歧义。
然而异常的出现会使情况更加复杂,例如,假设pod_bay_doors.open()
抛出了一个异常,我们不能肯定这等代码已经执行完了,所以不能直接转到下一行代码执行。因此,Python提供了try
、except
、finally
和else
来处理这种情况,它们几乎可以涵盖所有处理异常的方法。
函数的应用是另一种控制代码执行顺序的方法:
pprint(sorted(x.get_names()))
首先执行x.get_names()
,然后以它的返回值为参数来执行sorted
函数,再把sorted
返回的结果pprint
出来。上面的代码也可以写成:
names = x.get_names()
sorted_names = sorted(names)
pprint(sorted_names)
如果在执行下一行代码时前面的代码未必能执行完会怎么样?如果pod_bay_doors.open()
直接返回,只是激活了某个最终一定会打开分离舱门的东西、并使得Python解释器已经开始执行pod.lanuch()
,会怎么样?
也就是说,如果代码的执行顺序跟在Python文件里的顺序没有关系、或者“返回”并不意味着“完成”时会怎么样?
异步操作?
我们应该怎样阻止吊舱撞上关闭着的门?面对打开门失败的风险我们应当怎样做?要是打开门会给我们一些发射吊舱的信息会怎样?我们又该如何获取这些信息?
还有,既然我们是在讨论编程,我们又应该怎样来组织代码?
我们需要一种方法来使得”只有那个执行成功了之后再执行这个“。
我们需要一种方法来判断程序是执行成功了还是调用被中止了,也就是通常使用的try
、except
、finally
和else
。
我们需要一种机制来从执行失败的程序中传递失败相关的信息及异常到将要执行的程序中。
我们需要一种方法来操作还未得到的结果,不是具体地执行,而是计划一下可以执行的时候会怎么做。
除非破解了Python器,我们的解决方法只能建立在Python语言已有的结构上:方法、函数、对象之类。
或许我们需要的其实是这种代码:
placeholder = pod_bay_doors.open()
placeholder.when_done(pod.launch)
Twisted用来解决这个问题的方案是Deferred,一种用来描述将要做一件事(且只能是一件事)的对象,它描述了一种与Python文件中的位置无关的代码执行顺序。
这跟线程、并发、信号和子进程都无关,也不需要事件循环、协程或者调度。它只知道按照什么样的顺序来执行一件件事情,而这是我们显式地告诉它的。
对于这样的代码:
pod_bay_doors.open()
pod.launch()
可以改写成:
d = pod_bay_doors.open()
d.addCallback(lambda ignored: pod.launch())
这几行代码包含了许多新的概念,我们慢慢来拆解,如果你已经熟悉了这些概念,可以直接跳转到下一部分。
在这里,pod_bay_doors.open()
返回了一个Deferred
对象,我们把它赋值给d
。可以把d
当成一个占位符,代表着open()
函数终止有机会执行完之后返回的值。
然后我们给d
加了一个回调函数,回调函数就是以open()
函数最终返回的值为参数执行的函数。
现在我们已经把原先的按行执行的代码执行顺序换成了在代码中显式地控制的代码执行顺序,d
代表了代码的特定执行流,而d.addCallback
就相当于将要执行的”下一行代码”。
当然,程序的代码远远不止两行,而且目前为止我们也不知道如何处理执行失败的情况。
下面是一些把按行执行的代码转换成使用Deferred
对象控制执行顺序的例子。
回忆之前的代码:
pprint(sorted(x.get_names()))
也就是:
names = x.get_names()
sorted_names = sorted(names)
pprint(sorted_names)
如果get_names
和sorted
函数不能保证在它们返回之前就执行完毕那该怎么样?也就师说,如果它们都是异步函数该怎么样?
换成Twisted的表达方式它们会返回Deferred
对象,我们可以这么写:
d = x.get_names()
d.addCallback(sorted)
d.addCallback(pprint)
这样,sorted
函数会以get_names
函数的最终返回值为参数来调用,当它执行完毕的时候,它会把返回值传递给pprint
函数当做参数来调用。
由于d.addCallback
的返回值还是d
,所以以上代码也可以写成:
x.get_names().addCallback(sorted).addCallback(pprint)
我们经常需要写类似的代码:
try:
x.get_names()
except Exception, e:
report_error(e)
应该怎样用Deferred
对象来改写上面的代码呢?
d = x.get_names()
d.addErrback(report_error)
errback
是一个一个Twisted的术语,代表代码执行出错的时候调用的回调函数。
其实这里还隐藏了一个知识点,report_error
这个错误处理回调函数接受的参数是Faliure
对象,而不是普通的异常对象e
。Faliure
对象包含了所有异常对象包含的信息,但是为了与Deferred
对象配合使用而进行了优化。
在我们处理完所有的异常组合之后会更加深入地讲解这一点。
如果我们想在try
代码块顺利执行之后再执行一些代码该怎样做呢?例如:
try:
y = f()
except Exception, e:
g(e)
else:
h(y)
使用Deferred
对象后可以写成:
d = f() d.addCallbacks(h, g)
addCallbacks
的意思是给Deferred
对象同时加上回调函数和错误处理回调函数,h
是回调函数,而g
是错误处理回调函数。
现在有了addCallbacks
、addCallback
和addErrback
,就可以处理你要什么情况下的try
、except
、finally
和else
的组合了。
如果我们不管有没有出现犯错误都想在try/except
代码块后执行一些代码该怎么办?这是例子:
try:
y = f()
except Exception, e:
y = g(e)
h(y)
使用Deferred
对象:
d = f()
d.addErrback(g)
d.addCallback(h)
由于addErrback
返回的仍然是d
,所以可以写成:
f().addErrback(g).addCallback(h)
addErrback
和addCallback
函数的位置很重要,在下一部分我们会看到把它们换个位置会有什么样的影响。
如果我们想在一个异常处理代码中包含多步操作时应该怎样写?例子:
try:
y = f()
z = h(y)
except Exception, e:
g(e)
使用Deferred
对象:
d = f()
d.addCallback(h)
d.addErrback(g)
写成更简洁的方式:
d = f().addCallback(h).addErrback(g)
finally
语句中的代码怎么处理?如果不论异常发生与否都会执行这些代码怎么办?例子:
try:
y = f()
finally:
g()
粗略点可以这样写:
d = f() d.addBoth(g)
它把g
既设置成了回调函数又设置成了错误处理回调函数,和下面代码是一样的:
d.addCallbacks(g, g)
为什么说是“粗略点”呢?因为如果出现异常,g
会被传递一个Faliure
对象当做参数,其他情况下会被传递y
的值当做参数。
Twisted有一个装饰器叫做inlineCallbacks
,可以让你在使用Deferred
的同时不用写回调函数。这是通过把代码写成生成器来实现的,它不用关联回调函数,而是yield Deferred
对象。
看一下传统的Deferred
风格写出来的代码示例:
def getUsers():
d = makeRequest("GET", "/users")
d.addCallback(json.loads)
return d
使用了inlineCallbacks
可以写成:
from twisted.internet.defer import inlineCallbacks, returnValue
@inlineCallbacks
def getUsers(self):
responseBody = yield makeRequest("GET", "/users")
returnValue(json.loads(responseBody))
这里有几点需要注意:
makeRequest
返回的Deferred
对象上调用addCallback
函数,而是yield
了这个Deferred
对象。这样会使得Twisted直接返回给我们Deferred
对象的结果。returnValue
函数来返回函数的结果,因为这个函数是一个生成器,不能使用return
语句,否则会发生语法错误。注意,这是Twisted 15.0版本的新功能,在Python 3.3及以上版本中,还可以写成return json.loads(responseBody)
,这在可读性上面有所加强,但如果要兼容Python 2就不能这样写了。
以上两个版本的getUsers
函数对于它们的调用者来说都是相同的:它们都返回了一个Deferred
对象,并且由请求得来的JSON数据来激活。虽然说inlineCallbacks
看下来更像是同步的代码,因为它看起来在等待请求的响应时被阻塞了,其实每个yield
语句在等待yield
的Deferred
对象被激活时都是允许其他代码先执行的。
inlineCallbacks
在处理复杂的控制流及错误处理时更显示出它的强大之处。例如,如果makeRequest
因为连接失败而出错了怎么办?假设我们想在失败时记录下日志并返回一个空的列表:
def getUsers():
d = makeRequest("GET", "/users")
def connectionError(failure):
failure.trap(ConnectionError)
log.failure("makeRequest failed due to connection error",
failure)
return []
d.addCallbacks(json.loads, connectionError)
return d
如果使用了inlineCallbacks
,可以写成:
@inlineCallbacks
def getUsers(self):
try:
responseBody = yield makeRequest("GET", "/users")
except ConnectionError:
log.failure("makeRequest failed due to connection error")
returnValue([])
returnValue(json.loads(responseBody))
这样的错误处理程序显得更加简单,因为我们使用了Python传统的try/except
语句。
本文简单介绍了异步代码以及使用Deferred
对象可以用来:
inlineCallbacks
而不是回调函数来编写程序