2014-05-17 20 views
8

Ultimamente sto cazzeggiando con asyncio, e mentre sto iniziando a intuire come funziona, c'è qualcosa che non sono stato in grado di fare. Non sono sicuro se è perché ho sbagliato la costruzione, o se c'è una ragione per cui quello che sto cercando di fare non ha senso.Iterating over asyncio.coroutine

In breve, voglio essere in grado di scorrere su un asyncio.coroutine cedevole. Ad esempio, mi piacerebbe essere in grado di fare qualcosa di simile:

@asyncio.coroutine 
def countdown(n): 
    while n > 0: 
     yield from asyncio.sleep(1) 
     n = n - 1 
     yield n 

@asyncio.coroutine 
def do_work(): 
    for n in countdown(5): 
     print(n) 

loop.run_until_complete(do_work()) 

Tuttavia, questo genera un'eccezione dalle viscere della asyncio. Ho provato altre cose, come for n in (yield from countdown(5)): ... ma ciò fornisce anche un'eccezione di runtime simile opaco.

Non riesco a capire immediatamente perché non dovresti fare qualcosa del genere, ma sto arrivando ai limiti della mia capacità di capire cosa sta succedendo.

Quindi:

  • se è possibile farlo, come posso farlo?
  • se non è possibile, perché no?

Fatemi sapere se questa domanda non è chiara!

risposta

5

Nelle coroutine asyncio è necessario utilizzare yield from e mai yield. Questo è di progettazione. L'argomento per yield from dovrebbe essere un altro coroutine o solo l'istanza asyncio.Future.

Le chiamate di coroutine devono essere utilizzate con yield from nuovamente come yield from countdown(5).

Per il vostro caso mi consiglia di utilizzare le code:

import asyncio 

@asyncio.coroutine 
def countdown(n, queue): 
    while n > 0: 
     yield from asyncio.sleep(1) 
     n = n - 1 
     yield from queue.put(n) 
    yield from queue.put(None) 

@asyncio.coroutine 
def do_work(): 
    queue = asyncio.Queue() 
    asyncio.async(countdown(5, queue)) 
    while True: 
     v = yield from queue.get() 
     if v: 
      print(v) 
     else: 
      break 

asyncio.get_event_loop().run_until_complete(do_work()) 

Beh, è ​​possibile utilizzare verificare la presenza di valori prodotti dai countdown, il seguente esempio funziona. Ma penso che sia antipattern:

  1. troppo facile fare un pasticcio

  2. È in ogni caso non possono comporre countdown chiamate con, diciamo, itertools funzioni. Intendo qualcosa come sum(countdown(5)) o itertools.accumulate(countdown(5)).

Ad ogni modo, ad esempio con la miscelazione yield e yield from in coroutine:

import asyncio 

@asyncio.coroutine 
def countdown(n): 
    while n > 0: 
     yield from asyncio.sleep(1) 
     n = n - 1 
     yield n 

@asyncio.coroutine 
def do_work(): 
    for n in countdown(5): 
     if isinstance(n, asyncio.Future): 
      yield from n 
     else: 
      print(n) 

asyncio.get_event_loop().run_until_complete(do_work()) 
1

Aggiornamento: Sembra python 3.5 supports this even better natively:

essere bloccato con lo stesso problema (e ispirato dal codice in aio-s3), ho sentito che dovrebbe esserci una soluzione più elegante.

import asyncio 

def countdown(number): 
    @asyncio.coroutine 
    def sleep(returnvalue): 
     yield from asyncio.sleep(1) 
     return returnvalue 
    for n in range(number, 0, -1): 
     yield sleep(n) 

@asyncio.coroutine 
def print_countdown(): 
    for future in countdown(5): 
     n = yield from future 
     print ("Counting down: %d" % n) 

asyncio.get_event_loop().run_until_complete(print_countdown()) 

Rationale: Le countdown metodo fornisce future, ognuno si risolvono dopo un secondo sonno 1 al numero fornito.

La funzione print_countdown prende il primo futuro, yield from -ing esso (che sarà in pausa finché non è risolta) e ottenere il risultato desiderato: n.

4

In Python 3.5, viene introdotta la sintassi async for. Tuttavia, la sintassi della funzione iteratore asincrona è ancora assente (ad esempio yield è vietata nelle funzioni async). Ecco una soluzione:

import asyncio 
import inspect 

class escape(object): 
    def __init__(self, value): 
     self.value = value 

class _asynciter(object): 
    def __init__(self, iterator): 
     self.itr = iterator 
    async def __aiter__(self): 
     return self 
    async def __anext__(self): 
     try: 
      yielded = next(self.itr) 
      while inspect.isawaitable(yielded): 
       try: 
        result = await yielded 
       except Exception as e: 
        yielded = self.itr.throw(e) 
       else: 
        yielded = self.itr.send(result) 
      else: 
       if isinstance(yielded, escape): 
        return yielded.value 
       else: 
        return yielded 
     except StopIteration: 
      raise StopAsyncIteration 

def asynciter(f): 
    return lambda *arg, **kwarg: _asynciter(f(*arg, **kwarg)) 

Allora il vostro codice potrebbe essere scritto come:

@asynciter 
def countdown(n): 
    while n > 0: 
     yield from asyncio.sleep(1) 
     #or: 
     #yield asyncio.sleep(1) 
     n = n - 1 
     yield n 

async def do_work(): 
    async for n in countdown(5): 
     print(n) 

asyncio.get_event_loop().run_until_complete(do_work()) 

per conoscere la nuova sintassi, e come funziona questo codice, vedere PEP 492

+0

Ciò è impressionante! Ovviamente dovremmo usare Pyhton 3.6 dove i generatori asincroni sono supportati nativamente, ma se sei costretto a usare Python 3.5 per qualsiasi motivo, questo è un vero gioiello. È anche molto simile nella sintassi al modo in cui useresti i generatori asincroni in 3.6, quindi la transizione dovrebbe essere facile quando succede. –

+0

Come posso creare generatori asincroni nidificati in questo modo? Nel tuo esempio, cosa succede se 'do_work' stesso dovrebbe produrre risultati? Se lo decoriamo con '@ asynciter' invece di usare' async def', ovviamente non possiamo più usare 'async for'. Come facciamo a ripetere i risultati della coroutine interna? –

+0

In realtà, non importa, ho scoperto che c'è un fantastico pacchetto [async_generator] (https://pypi.python.org/pypi/async_generator/1.7) che fa già tutto questo! –