twisted系列教程十七–用inlineCallbacks来管理callbacks

Introduction

在这一部分我们继续回到callback.我们将介绍用生成器来写callbacks.我们会讲到这个技巧怎么工作的,还有它和Deferred 的比较.最后我们会用这个技巧重写我们的poetry client.首先我们要回顾一下生成器是怎样工作的,人后我们就会明白为什么它是创造callbacks 的一个替代品.

A Brief Review of Generators

你可能已经知道,python 的生成器是一个可以重新启动的函数,你可以用yield来实现重新启动的功能.通过yield,这个函数就变成了一个生成器函数,它会返回一个迭带器,并可以将一个函数分成一系列的步骤来运行.迭带器的每一个循环都会重启这个函数,这个函数会继续运行直到遇到下一个yield.

生成器(和迭带器)经常用来表示生成一系列的返回值.先看一下我们的例子inline-callbacks/gen-1.py:

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 的生成器.如果你运行这个代码的话,你会发现generator 里面的输出和for循环里面的输出是交替出现的.

我们可以自己实现生成器来让这段代码更明确,代码见inline-callbacks/gen-2.py:

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)
    当这个循环正在运行其他的代码(比如print 语句),这个生成器不会运行
    当这个生成器在运行的时候,这个循环不会运行(它等待generator 并阻塞)
    一但一个生成器失去对循环的控制, 可能会花任意数量的时间去执行其他的任务直到生成器再一次运行

这有点像callback 在异步程序中的工作方式.我们可以把循环看成reactor,把生成器看成一系列的callbacks.有意思的地方是,所有的callbacks 都共享同一个命名空间,命名空间从一个callback到另一个callback 得到了延续.

此外,我们可以在同一时间让多个生成器处于活跃状态(例子在inline-callbacks/gen-3.py),让它们的”callback”互相的交替运行,就像你在twisted 中又运行了一个独立的异步任务.

还漏了一点,callback不是被reactor调用的,callbacks同时也要接受参数.在一个deferred 链中,一个callback 或者接收一个结果,或者接收一个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 这个值会是None.你也可以在生成器中抛出一个异常.

Inline Callbacks

根据上面我们讲的在生成器中发送和抛出值和异常的原理,我们可以可以把生成器看成一系列的callbacks,就像deferred 中的那样,或接收一个正常的结果或接收一个failure.这些callbacks 被yield 分开,每一个yield 表达式的值是下一个callback(yield) 的参数.图像三十五描述了这个过程:


图片三十五

现在当一系列的callback在deferred中组成一个链的时候.每一个callback从上一个callback中获取结果.这在生成器中很简单–把你上一次运行生成器的的结果传到下一次运行生成器就可以了.

回想第十三部分我们所讲到的,deferred 中的callback 也可以返回deferred 对象,外部的deferred 会保持暂停直到内部的deferred 的触发,然后外部deferred中的下一个callback(errback)会被调用,并被传入内部deferred 返回的值.

所以想象我们的生成器返回的不是一个普通的python 值,而是一个deferred 对象.当这发生时,外部的deferred 暂停,直到内部的deferred触发.这里相当于生成器暂停,并且是是自动的,生成器在一个yield之后会一直暂停直到它被明确的重启.所以我们可以延迟重启生成器直到deferred 触发,在这时我们可以返回正常的值(deferred 正常)或者抛出异常(deferred 失败).这样我们的生成器就成为一个真正的异步callback 序列了,这也是inlineCallbacks 函数要完成的功能.代码在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 是一个装饰器并用来装饰生成器函数.inlineCallbacks 的主要的目的就是把一个生成器变成一系列的异步的callbacks.

第二,当我们调用一个用inlineCallbacks 修饰的函数的时候,我们不需要调用下一个或者发送或者抛出我们自己.这个装饰器会帮我们完成这些并会确保我们的生成器会一直运行到底(假设它并没有抛出异常).
第三,假如我们在生成器中生成一个不是deferred 的值,生成器会立即重启并带着这个yield 生成的值.
最后,如果我们在生成器中生成一个deferred,它会在这个deferred触发之后才会重启.如果这个deferred 成功了,yield 的结果就是deferred 的结果.如果这个deferred 失败了,yield 会抛出这个异常.注意这里的异常是一个普通的Exception 而不是Failure,我们可以用try/except 来捕捉它.

在这个例子中我们仅仅使用了callLater 在一段时间之后去触发deferred.这是一个很方便的把非阻塞的延迟放入callback 链的方法,一般来说,在我们的生成器中我们会不断的返回一个已经被触发过的deferred.

ok,我们现在已经知道一个被inlineCallbacks修饰的函数是怎样运行的,但这个函数最终会返回什么呢? 你可能已经猜到了,会返回deferred.因为我们不知道生成器什么时候会停止运行,这个被修饰过的函数是一个异步的函数,最适合返回的是deferred.注意这个返回的deferred 不是yield 语句返回的deferred,它是这个生成器全部运行完毕之后才触发的deferred.

如果这个生成器抛出一个异常,返回的deferred 会触发它的errback 链并带有一个封装了异常的Failure.如果我们想让生成器返回一个正常的值,我们必须用defer.returnValue 函数返回它.像平常的return 语句,它会停止这个生成器(实际上是抛出了一个特别的异常).inline-callbacks/inline-callbacks-2.py 例子描述了所有的情况.

Client 7.0

让我们用inlineCallbacks 来写我们的新的poetry client.你可以在twisted-client-7/get-poetry.py看到代码.你可能希望和client 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 负责获取到诗歌和应用改变.因为两种操作都是异步的,我们每一次yield 一个deferred 并等待结果.和client 6.0 中一样,如果transformation失败我们就返回原来的诗.注意我们可以用try/except 在生成器中处理异步的错误.

我们可以用和以前一样的方法测试新的client.首先开启一个transform server:

python twisted-server-1/tranformedpoetry.py --port 10001

然后开启两个poetry server:

python twisted-server-1/fastpoetry.py --port 10002 poetry/fascination.txt
python twisted-server-1/fastpoetry.py --port 10003 poetry/science.txt

现在你可以运行我们的新的client 了:

python twisted-client-7/get-poetry.py 10001 10002 10003

Discussion

就像deferred 对象,inlineCallbacks 函数给了我们一个组织我们的异步的callback 的方法.和deferred 相比,inlineCallbacks 没有改变游戏规则(不知道怎样翻译贴切).特别的,我们的callback仍是一次只运行一个,而且都是reactor触发的.我们可以通过在代码中输出堆栈信息来确认这一点,例子代码在inline-callbacks/inline-callbacks-tb.py.运行这个代码你会得到一个traceback, reactor.run()在上面,中间有很多帮助函数,我们的callback 在下面.

我们可以适当的改变图片二十九,它解释了当一个deferred 中的callback 返回了令一个deferred会发生什么.图片三十六描述了当一个inlineCallbacks 生成器生成一个deferred 会发生什么:


图片三十六

这个图片和图片二十九很像,因为它们表述的想法是一样的–一个异步的操作等待令一个.

因为inlineCallbacks 和 deferred 可以解决很多相同的问题,它们两个该选哪一个呢?下面是inlineCallbacks 可能存在的一些优势:

    可以让callbacks 共享一个命名空间,不用传过于的参数
    callback 的顺序很容易看到,因为它们从头执行到尾
    有的callback 不用具体的函数声明,明确地流程控制,可以让人少写代码
    错误可以被我们熟悉的try/except 来处理

 当然也有一些陷阱:

    在生成器中的callback不能个别的触发,这就样重用代码变得困难些.而在deferred,组成deferred 的代码可以任意的加callback
    生成器的形式模糊了一个异步的callbacks是成对(callback/errback)出现的事实.尽管它外表上看起来像一般的序列函数,一个生成器却表现出一种完全不同的行为.inlineCallbacks 函数是学习异步编程模型必不可少的

Summary

在这一部分我们学习了inlineCallbacks 装饰器和怎样用inlineCallbacks来组织多个callbacks.
在第十八部分 我们会学习一个可以管理多个并行的异步操作的方法.

你可能感兴趣的:(Python,Twisted)