用asyncio协程链接的方法

时间:2015-02-15 04:30:49

标签: python python-3.x python-asyncio

我想实现method chaining,但不是通常的函数 - 对于asyncio协同程序。

import asyncio


class Browser:
    @asyncio.coroutine
    def go(self):
        # some actions
        return self

    @asyncio.coroutine
    def click(self):
        # some actions
        return self

调用链的“直观”方式不起作用,因为单个方法返回coroutine(生成器),而不是self:

@asyncio.coroutine
def main():
    br = yield from Browser().go().click()  # this will fail

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

正确的呼叫链方式是:

br = yield from (yield from Browser().go()).click()

但它看起来很难看,并且在链条增长时变得难以理解。

有没有办法做得更好?欢迎任何想法。

2 个答案:

答案 0 :(得分:3)

我创建了一个解决方案,可以完成所需的工作。想法是使用Browser()的包装器,它使用__getattr____call__来收集动作(比如获取属性或调用)并返回self以捕获下一个动作。收集完所有操作后,我们会使用yiled from wrapper“抓住”__iter__并处理所有已收集的操作。

import asyncio


def chain(obj):
    """
    Enables coroutines chain for obj.
    Usage: text = yield from chain(obj).go().click().attr
    Note: Returns not coroutine, but object that can be yield from.
    """
    class Chain:
        _obj = obj
        _queue = []

        # Collect getattr of call to queue:
        def __getattr__(self, name):
            Chain._queue.append({'type': 'getattr', 'name': name})
            return self

        def __call__(self, *args, **kwargs):
            Chain._queue.append({'type': 'call', 'params': [args, kwargs]})
            return self

        # On iter process queue:
        def __iter__(self):
            res = Chain._obj
            while Chain._queue:
                action = Chain._queue.pop(0)
                if action['type'] == 'getattr':
                    res = getattr(res, action['name'])
                elif action['type'] == 'call':
                    args, kwargs = action['params']
                    res = res(*args, **kwargs)
                if asyncio.iscoroutine(res):
                    res = yield from res
            return res
    return Chain()

用法:

class Browser:
    @asyncio.coroutine
    def go(self):
        print('go')
        return self

    @asyncio.coroutine
    def click(self):
        print('click')
        return self

    def text(self):
        print('text')
        return 5


@asyncio.coroutine
def main():
    text = yield from chain(Browser()).go().click().go().text()
    print(text)


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

输出:

go
click
go
text
5

注意,chain()不会返回真正的协同程序,而是可以像yield from上的协程一样使用的对象。我们应该将chain()的结果包装起来以获得正常的协同程序,它可以传递给任何需要协同程序的asyncio函数:

@asyncio.coroutine
def chain_to_coro(chain):
    return (yield from chain)


@asyncio.coroutine
def main():
    ch = chain(Browser()).go().click().go().text()
    coro = chain_to_coro(ch)

    results = yield from asyncio.gather(*[coro], return_exceptions=True)
    print(results)

输出:

go
click
go
text
[5]

答案 1 :(得分:2)

它仍然不是特别漂亮,但你可以实现一个更好一点的chain函数:

import asyncio  

@asyncio.coroutine
def chain(obj, *funcs):
    for f, *args in funcs:
        meth = getattr(obj, f)  # Look up the method on the object
        obj = yield from meth(*args) 
    return obj

class Browser:
    @asyncio.coroutine
    def go(self, x, y):
        return self

    @asyncio.coroutine
    def click(self):
        return self


@asyncio.coroutine
def main():
        #br = yield from (yield from Browser().go(3, 4)).click()
        br = yield from chain(Browser(), 
                                ("go", 3, 4),
                                ("click",))

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

我们的想法是将(method_name, arg1, arg2, argX)格式的元组传递给chain函数,而不是实际链接方法调用本身。如果您不需要支持将参数传递给链中的任何方法,则可以直接传递方法名称。