我想实现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()
但它看起来很难看,并且在链条增长时变得难以理解。
有没有办法做得更好?欢迎任何想法。
答案 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
函数,而不是实际链接方法调用本身。如果您不需要支持将参数传递给链中的任何方法,则可以直接传递方法名称。