Attendre sans rester immobile
La plupart du temps, un programme de données ne calcule pas, il attend : la réponse d'un serveur de prix, l'accusé d'un ordre, la fin d'un téléchargement. S'il demande dix prix un par un, il attend dix fois de suite. Le code asynchrone permet à un programme de lancer une requête, de la mettre de côté pendant qu'elle attend et de passer à une autre, le tout dans un seul fil. L'outil Python pour cela est asyncio, construit sur deux mots-clés, async def et await.
# A coroutine: calling it does nothing until it is awaited
import asyncio
async def get_price(symbol):
await asyncio.sleep(0.01) # stands in for waiting on a network answer
return {"AAPL": 184.25, "MSFT": 410.5}[symbol]
coro = get_price("AAPL")
print(type(coro).__name__) # coroutine nothing has run yet
print(asyncio.run(coro)) # 184.25 the event loop runs itasync def, await et la boucle d'événements
Une fonction définie avec async def est une fonction coroutine : l'appeler renvoie un objet coroutine et n'exécute rien de son corps. À l'intérieur, await quelquechose met la coroutine en pause jusqu'à ce que quelquechose soit prêt et rend la main à la boucle d'événements, qui peut exécuter d'autres coroutines entre-temps. asyncio.run(coro) démarre une boucle et exécute une coroutine jusqu'au bout. await ne s'emploie que dans une async def ; l'oublier est l'erreur classique, car l'appel renvoie alors une coroutine au lieu de la réponse.
# Awaiting ten prices one after another waits ten times; gather overlaps the waits
import asyncio
import time
async def get_price(symbol):
await asyncio.sleep(0.05)
return len(symbol) * 10.0
async def one_by_one(symbols):
return [await get_price(s) for s in symbols]
async def together(symbols):
return await asyncio.gather(*(get_price(s) for s in symbols))
symbols = ["AAPL", "MSFT", "NVDA", "TSLA", "AMZN", "META", "GOOG", "NFLX", "ORCL", "INTC"]
t0 = time.perf_counter()
asyncio.run(one_by_one(symbols))
slow = time.perf_counter() - t0
t0 = time.perf_counter()
asyncio.run(together(symbols))
fast = time.perf_counter() - t0
print(f"one by one: {slow:.2f} s, gathered: {fast:.2f} s") # about 0.50 s against 0.05 s
print(slow > 4 * fast) # TrueErreurs, délais et faire à la main
L'attente peut échouer. Une requête qui ne répond jamais doit être coupée avec asyncio.wait_for(coro, timeout), qui lève TimeoutError ; un await dans un try / except gère les erreurs exactement comme du code ordinaire, donc une nouvelle tentative est une boucle autour d'un appel attendu. Sous le capot, une coroutine est simplement quelque chose qu'on fait avancer : coro.send(None) l'exécute jusqu'à sa prochaine pause et, quand elle se termine, lève StopIteration portant le résultat. Une boucle d'événements est le code qui répète cela pour de nombreuses coroutines à la fois, et c'est aussi ainsi que les tests de ce labo exécutent les vôtres.
# Stepping a coroutine by hand: this is what an event loop does for many of them
class Pause:
def __await__(self):
yield
async def job():
await Pause()
await Pause()
return 184.25
coro = job()
steps = 0
while True:
try:
coro.send(None)
except StopIteration as stop:
print("finished with", stop.value) # finished with 184.25
break
steps += 1
print("paused", steps, "times") # paused 2 times# A timeout turns a slow answer into an error you can handle, then retry
import asyncio
async def slow_feed():
await asyncio.sleep(1.0)
return 184.25
async def main():
for attempt in range(1, 4):
try:
return await asyncio.wait_for(slow_feed(), timeout=0.01)
except TimeoutError:
print(f"attempt {attempt} timed out")
return None
print(asyncio.run(main())) # three timeouts, then Noneasyncio n'exécute qu'une coroutine à la fois et ne change qu'à un await. Il accélère l'attente du réseau, pas l'arithmétique lourde : un long calcul dans une coroutine bloque la boucle et toutes les autres coroutines avec. Faites le calcul dans des fonctions ordinaires et gardez les coroutines pour l'attente.async defdéfinit une fonction coroutine ; l'appeler crée seulement une coroutine, rien ne s'exécute encore.awaitmet en pause jusqu'à ce que le résultat soit prêt et laisse la boucle d'événements en exécuter d'autres ; il ne marche que dansasync def.asyncio.rundémarre la boucle,gatherchevauche les attentes,wait_forajoute un délai ; les erreurs se gèrent avec untry/exceptordinaire.
À suivre : le flux de données avec les websockets, recevoir un flux de ticks en direct au fur et à mesure.
Lab : ce qui est noté
- Attendre une cotation avec
get_price25 pts - Récupérer une liste de suivi avec
get_prices25 pts - Survivre à un délai dépassé avec
retry_price25 pts - Jouer la boucle d'événements avec
drive25 pts