2015-02-15 11 views
5

Chcę zaimplementować method chaining, ale nie dla zwykłych funkcji - dla asyncio coroutines.Metoda łańcuchowa z asyncio coroutines

import asyncio 


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

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

"intuicyjny" sposób wywołać łańcuch nie będzie działać, ponieważ pojedyncza metoda zwraca współprogram (generator), bez własnego:

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

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

poprawny sposób wywołać łańcuch jest:

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

Ale wygląda to paskudnie i staje się nieczytelne, gdy łańcuch rośnie.

Czy jest jakiś sposób, aby to zrobić lepiej? Wszelkie pomysły są mile widziane.

+1

Nie jestem pewien, czy rozumiem co próbujesz do zrobienia, ale jeśli chcesz tylko iterować metody na obiekcie, możesz to zrobić, ustawiając je w dyktafonie lub używając 'getattr'. – user3467349

+1

Przepraszamy, powinieneś użyć "brzydkiego" sposobu. –

Odpowiedz

3

Stworzyłem rozwiązanie, które wykonuje pracę w pobliżu potrzebnych. Pomysł polega na użyciu wrappera dla Browser(), który używa __getattr__ i __call__ do zbierania akcji (jak uzyskanie atrybutu lub połączenia) i powrotu do siebie, aby złapać następną akcję. Po zebraniu wszystkich akcji "łapiemy" yiled from wrapper używając __iter__ i przetwarzamy wszystkie zebrane akcje.

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

Zastosowanie:

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

wyjściowa:

go 
click 
go 
text 
5 

Zauważ, że chain() nie wraca prawdziwy współprogram, ale obiekt, który może być używany jak współprogram na yield from. powinniśmy owinąć wynik chain() dostać normalną współprogram, które mogą być przekazane do dowolnej funkcji asyncio wymagającej współprogram:

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

wyjściowa:

go 
click 
go 
text 
[5] 
2

To wciąż nie jest szczególnie ładne, ale można wdrożyć chain funkcję, która skaluje się trochę lepiej:

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

Chodzi o to, aby przekazać krotki w formacie (method_name, arg1, arg2, argX) do funkcji chain, zamiast faktycznie łańcuchowym sama metoda wywołuje. Możesz po prostu przekazać nazwy metod bezpośrednio, jeśli nie potrzebujesz obsługi argumentów przechodzących do żadnej z metod w łańcuchu.