Variáveis de contexto
Esta aula explora o módulo contextvars do Python, que permite armazenar e gerenciar variáveis de contexto, especialmente úteis em programação assíncrona. Você aprenderá a criar, definir e acessar variáveis de contexto, comparar com thread-local e ver exemplos práticos de uso em código async.
Variáveis de contexto são um mecanismo do Python para armazenar dados que devem ser isolados entre diferentes contextos de execução, como corrotinas, tarefas ou threads. O módulo contextvars, introduzido na versão 3.7, fornece uma API para criar e manipular essas variáveis de forma segura, garantindo que cada contexto tenha seu próprio valor.
Em programação assíncrona, especialmente com asyncio, é comum precisar de informações que pertencem a uma requisição ou tarefa específica, como um ID de usuário, um token de autenticação ou configurações de logging. Variáveis de contexto resolvem esse problema sem precisar passar esses dados explicitamente por todos os parâmetros das funções, tornando o código mais limpo e menos acoplado.
contextvars
O módulo contextvars fornece a classe ContextVar para declarar uma nova variável de contexto. Você cria uma instância no nível do módulo e a utiliza em qualquer lugar do seu código. Cada ContextVar tem um nome (para fins de debug) e um valor padrão opcional.
Para acessar o valor atual, use o método get(). Para definir um novo valor, use o método set(). É importante notar que set() retorna um token, que pode ser usado para reverter a alteração (como um mecanismo de reset). Isso é útil em cenários onde você deseja temporariamente mudar o valor e depois restaurar o anterior.
Exemplo básico:
import contextvars
# Cria uma variável de contexto com valor padrão
user_id = contextvars.ContextVar('user_id', default=None)
# Obtém o valor atual (None)
print(user_id.get())
# Define um novo valor
token = user_id.set(42)
print(user_id.get()) # 42
# Restaura o valor anterior
user_id.reset(token)
print(user_id.get()) # None
O método get() pode receber um valor padrão como argumento, que é usado se a variável não tiver valor definido no contexto atual. Por exemplo: user_id.get('desconhecido').
Além disso, o módulo fornece a classe Context, que representa um conjunto de valores de variáveis de contexto. Você pode copiar um contexto, executar código em um contexto específico usando Context.run(), ou criar um novo contexto vazio com contextvars.Context().
ctx = contextvars.Context()
ctx.run(user_id.set, 99) # executa a função set no contexto ctx
print(user_id.get()) # None, pois o valor foi definido apenas no ctx
Casos de uso (async)
Em aplicações assíncronas, cada tarefa (Task) ou corrotina (Coroutine) tem seu próprio contexto. Isso significa que, ao usar asyncio.create_task(), a nova tarefa herda uma cópia do contexto atual, mas as alterações feitas dentro dela não afetam o contexto externo. Isso é essencial para isolar dados entre requisições concorrentes.
Um caso comum é ter um middleware em um servidor web (como FastAPI ou aiohttp) que captura o ID do usuário autenticado e o armazena em uma variável de contexto. Depois, qualquer função chamada dentro daquela requisição pode acessar esse ID sem precisar recebê-lo como parâmetro.
Exemplo:
import asyncio
import contextvars
usuario_atual = contextvars.ContextVar('usuario_atual', default='anonimo')
async def processar_requisicao():
# Simula uma requisição
usuario_atual.set('joao')
await asyncio.sleep(0.1)
print(f'Requisição processada por {usuario_atual.get()}')
async def main():
# Cria duas tarefas concorrentes
task1 = asyncio.create_task(processar_requisicao())
task2 = asyncio.create_task(processar_requisicao())
await asyncio.gather(task1, task2)
asyncio.run(main())
Neste exemplo, cada tarefa define seu próprio valor para usuario_atual, e as impressões mostram que cada uma mantém seu valor isolado. Isso é fundamental para evitar que dados de uma requisição vazem para outra.
Outro caso é o rastreamento distribuído (tracing), onde você quer propagar um ID de correlação por todas as operações assíncronas de uma mesma requisição, sem passá-lo explicitamente.
vs thread-local
Antes de contextvars, a forma comum de isolamento era usar threading.local(), que armazena dados por thread. Em aplicações com threads, cada thread tem seu próprio espaço de armazenamento. No entanto, em programação assíncrona, as corrotinas são executadas em uma única thread, e o threading.local() não funciona corretamente porque todas as corrotinas compartilham a mesma thread.
Por exemplo, se você usar threading.local() dentro de um loop asyncio, todas as tarefas verão o mesmo valor, pois compartilham a thread. Isso quebra o isolamento esperado.
Com contextvars, o contexto é vinculado à tarefa ou corrotina, não à thread. Isso permite que múltiplas corrotinas concorrentes na mesma thread tenham valores diferentes para a mesma variável.
Além disso, contextvars funciona bem com asyncio, pois o próprio loop de eventos gerencia a troca de contexto quando há mudança de tarefa. Também é possível usar contextvars em código síncrono, mas o benefício é maior em código assíncrono.
Comparação rápida:
threading.local(): isolamento por thread; não funciona adequadamente em código async.contextvars: isolamento por contexto (tarefa/corrotina); funciona em código síncrono e assíncrono.
Exemplos
Vamos explorar alguns exemplos práticos de uso de contextvars.
Exemplo 1: Logging com contexto
Você pode incluir informações de contexto em mensagens de log, como o ID da requisição ou o usuário atual.
import contextvars
import logging
request_id = contextvars.ContextVar('request_id', default='-')
logger = logging.getLogger(__name__)
async def handle_request():
request_id.set('abc123')
logger.info('Iniciando processamento')
# ...
# Configurar um filtro para adicionar o request_id ao log
class RequestIdFilter(logging.Filter):
def filter(self, record):
record.request_id = request_id.get()
return True
logger.addFilter(RequestIdFilter())
Exemplo 2: Middleware em FastAPI
Um uso comum é em frameworks web como FastAPI, onde você pode capturar o usuário autenticado e armazená-lo em uma variável de contexto.
import contextvars
from fastapi import FastAPI, Request
app = FastAPI()
current_user = contextvars.ContextVar('current_user', default=None)
@app.middleware("http")
async def add_user_to_context(request: Request, call_next):
# Suponha que o usuário seja obtido de um token ou sessão
user = await get_user_from_request(request)
current_user.set(user)
response = await call_next(request)
return response
@app.get("/")
async def read_root():
user = current_user.get()
return {"message": f"Olá, {user.name}"}
Exemplo 3: Propagação de contexto em tarefas
Quando você cria uma nova tarefa com asyncio.create_task(), ela herda uma cópia do contexto atual. Isso é útil para propagar informações como IDs de correlação.
import asyncio
import contextvars
correlation_id = contextvars.ContextVar('correlation_id', default=None)
async def worker():
print(f'Worker vê correlation_id: {correlation_id.get()}')
async def main():
correlation_id.set('corr-123')
task = asyncio.create_task(worker())
await task
asyncio.run(main())
Neste caso, o worker verá o valor 'corr-123' porque herdou o contexto da tarefa principal.
Boas práticas
Ao usar contextvars, algumas boas práticas podem ajudar:
- Defina as variáveis de contexto no nível do módulo para que sejam acessíveis em todo o código.
- Use valores padrão sensatos para evitar erros quando o valor não foi definido.
- Evite armazenar objetos mutáveis em variáveis de contexto, pois podem causar efeitos colaterais inesperados.
- Em bibliotecas, exponha as variáveis de contexto como parte da API pública, mas documente claramente.
- Use
ContextVar.set()com cuidado em código assíncrono, pois o valor é restrito ao contexto atual.
Referências
- Documentação oficial do módulo contextvars
- Documentação do asyncio (Tasks)
- PEP 567 – Context Variables
- Real Python: Context Variables
- FastAPI: Middleware
- Documentação de threading.local
Exercícios
-
Crie uma variável de contexto chamada
configcom valor padrãoNone. Em seguida, defina um valor para ela e imprima. Depois, redefina paraNoneusando o token.✓ Resposta:import contextvars config = contextvars.ContextVar('config', default=None) # Define um valor token = config.set({'debug': True}) print(config.get()) # {'debug': True} # Restaura o valor padrão config.reset(token) print(config.get()) # None -
Escreva um programa assíncrono que use uma variável de contexto para armazenar um número. Crie duas tarefas que definem valores diferentes e imprimem o valor após um pequeno atraso. Verifique que cada tarefa vê seu próprio valor.
✓ Resposta:import asyncio import contextvars numero = contextvars.ContextVar('numero', default=0) async def tarefa(n): numero.set(n) await asyncio.sleep(0.1) print(f'Tarefa {n} vê: {numero.get()}') async def main(): await asyncio.gather(tarefa(1), tarefa(2)) asyncio.run(main()) -
Explique por que
threading.local()não é adequado para programação assíncrona e comocontextvarsresolve esse problema.✓ Resposta:Em programação assíncrona, todas as corrotinas são executadas em uma única thread (se você usar asyncio).
threading.local()armazena dados por thread, então todas as corrotinas compartilhariam o mesmo valor, quebrando o isolamento.contextvarscria um contexto por tarefa/corrotina, permitindo que cada uma tenha seu próprio valor, mesmo na mesma thread. -
Implemente um middleware simples para um servidor web (pode ser em FastAPI) que armazena o endereço IP do cliente em uma variável de contexto. Depois, em uma rota, acesse esse valor.
✓ Resposta:import contextvars from fastapi import FastAPI, Request app = FastAPI() client_ip = contextvars.ContextVar('client_ip', default='desconhecido') @app.middleware("http") async def capture_ip(request: Request, call_next): client_ip.set(request.client.host) response = await call_next(request) return response @app.get("/") async def root(): return {"ip": client_ip.get()} -
Dado o seguinte código, o que será impresso? Explique o resultado.
import asyncio import contextvars var = contextvars.ContextVar('var', default='padrão') async def altera(): var.set('alterado') await asyncio.sleep(0.1) print(f'dentro: {var.get()}') async def main(): print(f'antes: {var.get()}') task = asyncio.create_task(altera()) await asyncio.sleep(0.05) print(f'depois: {var.get()}') await task asyncio.run(main())✓ Resposta:Será impresso:
antes: padrão depois: padrão dentro: alteradoIsso ocorre porque a tarefa
altera()tem seu próprio contexto, herdado do contexto em que foi criada, mas as alterações dentro dela não afetam o contexto externo. Então,var.get()no main continua sendo 'padrão'.