Python : os gestores de contexto não servem apenas para abrir ficheiros

Os gestores de contexto permitem ao Python garantir que um recurso será limpo, quer o código do bloco tenha sucesso quer falhe. Um artigo técnico detalha o seu funcionamento sob a instrução with, assim como várias formas de os construir e utilizar, muito para além da gestão de ficheiros.

Muitos programadores descobrem este mecanismo com with open(path), que fecha o ficheiro automaticamente. Segundo o autor, os ficheiros são, contudo, o caso menos interessante: a instrução with delimita sobretudo um bloco cuja saída é garantida. O protocolo assenta em dois métodos, __enter__(), chamado à entrada do bloco, e __exit__(), chamado à saída, seja ela normal, causada por um return ou devida a uma exceção.

O valor devolvido por __exit__() desempenha um papel decisivo. Se for verdadeiro, a exceção é suprimida, o que pode passar despercebido: o programa continua, e o erro desaparece sem deixar rasto. Se for False ou estiver ausente, a exceção propaga-se após a limpeza. É por isso que os gestores apresentados no artigo nunca devolvem um valor verdadeiro.

Construir os seus próprios gestores

Uma classe é adequada quando o gestor precisa de manter um estado que o chamador pode reler. O artigo dá o exemplo de um temporizador, que mede a duração de um bloco graças a time.perf_counter() e exibe o resultado mesmo que o bloco falhe. Uma extração de características interrompida exibe assim «feature extraction: 12.418s (failed)», antes de a exceção se propagar.

Para uma tarefa linear, ou seja, uma configuração, um bloco e depois uma restauração, o decorador @contextmanager permite escrever uma simples função geradora. Tudo o que precede o yield executa-se à entrada, tudo o que segue à saída. O artigo recomenda colocar a restauração num finally, e nunca engolir a exceção num try/except sem a relançar. Apenas um valor deve ser produzido pelo yield, caso contrário é levantado um erro RuntimeError.

Restaurar o estado e gerir vários recursos

A biblioteca padrão fornece já vários gestores. TemporaryDirectory() cria um diretório com um nome único e elimina-o no final do bloco, o que evita ficheiros residuais de uma execução anterior. Desde o Python 3.12, o parâmetro delete=False permite conservar os ficheiros para uma análise posterior. No Windows, um ficheiro que permaneça aberto pode fazer falhar a eliminação; o parâmetro ignore_cleanup_errors=True, disponível desde o Python 3.10, atenua este problema sem o resolver.

Para modificações de dicionários, patch.dict(), proveniente do módulo unittest.mock, restaura o estado original após o bloco: uma chave existente retoma o seu valor, e uma chave ausente é eliminada. O artigo salienta, contudo, que os.environ é global ao processo. Todas as tarefas e todas as threads veem então a modificação, o que a torna arriscada em código concorrente.

Quando o número de recursos só é conhecido em tempo de execução, por exemplo o número de ficheiros a fundir, ExitStack agrupa os gestores. O seu método enter_context() regista um gestor, e callback() uma simples função. À saída, os elementos são desempilhados por ordem inversa. O artigo especifica que este desempilhamento ocorre no final do bloco ou durante uma chamada a close(), e não na eliminação do objeto pelo coletor de lixo. Uma classe dedicada pode ser preferível quando a pilha se torna demasiado pesada.