2015-02-15 19 views
5

Ich möchte method chaining implementieren, aber nicht für die üblichen Funktionen - für asyncio Koroutinen.Methode Verketten mit asyncio Korotinen

import asyncio 


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

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

"intuitive" way Kette nennen würde nicht funktionieren, da einzelne Methode gibt Koroutine (Generator) nicht selbst:

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

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

Correct Weise Kette zu nennen ist:

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

Aber es sieht hässlich aus und wird unlesbar, wenn die Kette wächst.

Gibt es eine Möglichkeit, dies besser zu tun? Irgendwelche Ideen sind willkommen.

+1

Ich bin nicht sicher, ob ich verstehe, was Sie versuchen, zu tun, aber wenn Sie nur Methoden für das Objekt iterieren wollen, können Sie das tun, indem Sie sie in ein dict oder mit 'getattr 'setzen. – user3467349

+1

Entschuldigung, Sie sollten den "hässlichen" Weg verwenden. –

Antwort

3

Ich erstellte eine Lösung, die einen Job in der Nähe der benötigten. Idee ist Wrapper für Browser() zu verwenden, die __getattr__ und __call__ verwendet, um Aktion zu sammeln (wie Attribut oder Aufruf erhalten) und selbst zurückgeben, um die nächste Aktion zu erfassen. Nachdem alle Aktionen gesammelt sind, "fangen" wir yiled from wrapper mit __iter__ und verarbeiten alle gesammelten Aktionen.

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

Verbrauch:

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

Ausgang:

go 
click 
go 
text 
5 

Hinweis, dass chain() nicht real Koroutine zurückkehrt, sondern Objekt, das wie Koroutine auf yield from verwendet werden kann. Wir sollten Ergebnis chain() wickeln normalen Koroutine zu erhalten, die für jede asyncio Funktion übergeben werden können, die Koroutine erfordert:

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

Ausgang:

go 
click 
go 
text 
[5] 
2

Es ist immer noch nicht besonders hübsch, aber man konnte eine chain Funktion implementieren, die ein wenig besser skaliert:

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

Die Idee ist Tupeln in einem (method_name, arg1, arg2, argX) Format an die chain Funktion, eher zu passieren, als tatsächlich Verkettungs Die Methode ruft sich selbst auf. Sie können die Methodennamen einfach direkt übergeben, wenn Sie die Übergabe von Argumenten an eine der Methoden in der Kette nicht unterstützen müssen.