Twisted之Deferred(一)

本文的目的是介绍Deferred,Twisted的控制异步代码流的机制,不要求你有Twisted的相关知识,只需知道知道Python语法即可。

代码的执行顺序

写Python代码时,一个很流行的常识就是同一个代码块中某行代码,只有在它前面的一行代码执行完毕之后才会轮到它执行:

pod_bay_doors.open()
pod.launch()

只有分离舱门打开,然后吊舱才能发射。一行接着一行地执行是Python内建的执行代码的机制,这样执行代码清楚、简洁而且没有歧义。

然而异常的出现会使情况更加复杂,例如,假设pod_bay_doors.open()抛出了一个异常,我们不能肯定这等代码已经执行完了,所以不能直接转到下一行代码执行。因此,Python提供了tryexceptfinallyelse来处理这种情况,它们几乎可以涵盖所有处理异常的方法。

函数的应用是另一种控制代码执行顺序的方法:

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文件里的顺序没有关系、或者“返回”并不意味着“完成”时会怎么样?

异步操作?

我们应该怎样阻止吊舱撞上关闭着的门?面对打开门失败的风险我们应当怎样做?要是打开门会给我们一些发射吊舱的信息会怎样?我们又该如何获取这些信息?

还有,既然我们是在讨论编程,我们又应该怎样来组织代码?

解决问题的要点

我们需要一种方法来使得”只有那个执行成功了之后再执行这个“。
我们需要一种方法来判断程序是执行成功了还是调用被中止了,也就是通常使用的tryexceptfinallyelse
我们需要一种机制来从执行失败的程序中传递失败相关的信息及异常到将要执行的程序中。
我们需要一种方法来操作还未得到的结果,不是具体地执行,而是计划一下可以执行的时候会怎么做。
除非破解了Python器,我们的解决方法只能建立在Python语言已有的结构上:方法、函数、对象之类。
或许我们需要的其实是这种代码:

placeholder = pod_bay_doors.open()
placeholder.when_done(pod.launch)

一种解决方案:Deferred

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_namessorted函数不能保证在它们返回之前就执行完毕那该怎么样?也就师说,如果它们都是异步函数该怎么样?

换成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对象,而不是普通的异常对象eFaliure对象包含了所有异常对象包含的信息,但是为了与Deferred对象配合使用而进行了优化。

在我们处理完所有的异常组合之后会更加深入地讲解这一点。

处理出错,但是执行成功后做其他的事情

如果我们想在try代码块顺利执行之后再执行一些代码该怎样做呢?例如:

try:
    y = f()
except Exception, e:
    g(e)
else:
    h(y)

使用Deferred对象后可以写成:

d = f() d.addCallbacks(h, g)

addCallbacks的意思是给Deferred对象同时加上回调函数和错误处理回调函数,h是回调函数,而g是错误处理回调函数。

现在有了addCallbacksaddCallbackaddErrback,就可以处理你要什么情况下的tryexceptfinallyelse的组合了。

只处理出错,然后执行

如果我们不管有没有出现犯错误都想在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) 

addErrbackaddCallback函数的位置很重要,在下一部分我们会看到把它们换个位置会有什么样的影响。

处理整个操作中的错误

如果我们想在一个异常处理代码中包含多步操作时应该怎样写?例子:

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的值当做参数。

内联回调函数——使用yield

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))

这里有几点需要注意:

  1. 我们没有在makeRequest返回的Deferred对象上调用addCallback函数,而是yield了这个Deferred对象。这样会使得Twisted直接返回给我们Deferred对象的结果。
  2. 我们使用了returnValue函数来返回函数的结果,因为这个函数是一个生成器,不能使用return语句,否则会发生语法错误。

注意,这是Twisted 15.0版本的新功能,在Python 3.3及以上版本中,还可以写成return json.loads(responseBody),这在可读性上面有所加强,但如果要兼容Python 2就不能这样写了。

以上两个版本的getUsers函数对于它们的调用者来说都是相同的:它们都返回了一个Deferred对象,并且由请求得来的JSON数据来激活。虽然说inlineCallbacks看下来更像是同步的代码,因为它看起来在等待请求的响应时被阻塞了,其实每个yield语句在等待yieldDeferred对象被激活时都是允许其他代码先执行的。

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对象可以用来:

  1. 在某个异步操作成功完成时执行一些代码
  2. 使用已经完成的异步操作的结果
  3. 在异步操作中捕捉异常
  4. 在操作成功时执行一些代码,失败时执行另外一些
  5. 在错误已被成功处理后执行一些代码
  6. 使用一个错误处理器捕捉多个步骤的错误
  7. 不管某个异步操作是否成功完成都执行一些代码
  8. 使用inlineCallbacks而不是回调函数来编写程序

你可能感兴趣的:(python)