Esperar sem ficar parado
Na maior parte do tempo um programa de dados não calcula, espera: o servidor de preços responder, uma ordem ser confirmada, um download terminar. Se pede dez preços um a um, espera dez vezes seguidas. O código assíncrono permite que um programa inicie um pedido, o deixe de lado enquanto espera e siga com outro, tudo numa única thread. A ferramenta do Python para isso é o asyncio, construído com duas palavras-chave, async def e 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 e o laço de eventos
Uma função definida com async def é uma função corrotina: chamá-la devolve um objeto corrotina e não executa nada do corpo. Dentro, await algo pausa a corrotina até algo estar pronto e devolve o controle ao laço de eventos, que pode executar outras corrotinas nesse meio-tempo. asyncio.run(coro) inicia um laço e executa uma corrotina até o fim. await só vale dentro de uma async def; esquecê-lo é o erro clássico, pois a chamada devolve uma corrotina em vez da resposta.
# 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) # TrueErros, timeouts e fazer à mão
A espera pode falhar. Um pedido que nunca responde deve ser cortado com asyncio.wait_for(coro, timeout), que levanta TimeoutError; um await dentro de try / except trata erros exatamente como código comum, então uma nova tentativa é um laço em volta de uma chamada aguardada. Por baixo, uma corrotina é algo que se pode avançar passo a passo: coro.send(None) a executa até a próxima pausa e, ao terminar, levanta StopIteration com o resultado. Um laço de eventos é o código que faz isso sem parar com muitas corrotinas ao mesmo tempo, e é assim também que os testes deste laboratório executam as suas.
# 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 executa uma corrotina por vez e só troca em um await. Acelera a espera da rede, não a aritmética pesada: um cálculo longo dentro de uma corrotina bloqueia o laço e todas as outras com ele. Faça a matemática em funções comuns e deixe as corrotinas para a espera.async defdefine uma função corrotina; chamá-la só cria uma corrotina, nada executa ainda.awaitpausa até o resultado estar pronto e deixa o laço de eventos executar outras; só funciona dentro deasync def.asyncio.runinicia o laço,gathersobrepõe as esperas,wait_foradiciona um tempo limite; erros se tratam comtry/exceptcomum.
A seguir: dados em streaming com websockets, receber um fluxo de ticks ao vivo conforme acontecem.
Lab: o que é avaliado
- Esperar uma cotação com
get_price25 pts - Buscar uma lista de acompanhamento com
get_prices25 pts - Sobreviver a um timeout com
retry_price25 pts - Ser o laço de eventos com
drive25 pts