Trilha de aprendizado · Nível 11 · Tutorial 10

Criar gerenciadores de contexto com classes

Ao concluir, você poderá implementar o protocolo usado por with para adquirir e liberar recursos, incluindo caminhos de falha sem supressão acidental de exceções.

  • Nível: Avançado
  • Duração: 25 min
  • 8 passos
Criar gerenciadores de contexto com classes

O que você vai percorrer

  1. Delimitar a responsabilidade pelo recurso Entenda o ciclo de vida que um gerenciador de contexto organiza antes de implementar seus métodos especiais. 2 min
  2. Implementar __enter__ e escolher o valor de as Configure o gerenciador, adquira o recurso no momento certo e escolha conscientemente o objeto que o bloco receberá após as. 3 min
  3. Implementar a saída normal e excepcional Faça o gerenciador liberar o recurso depois de uma entrada bem-sucedida e reconheça os dados recebidos em cada forma de saída do bloco. 4 min
  4. Controlar a propagação sem esconder erros Escolha o retorno de __exit__ de modo que exceções do bloco sejam preservadas por padrão e só sejam suprimidas quando isso fizer parte do contrato. 3 min
  5. Limpar aquisições parciais quando a entrada falha Proteja o recurso adquirido em __enter__ quando uma etapa posterior de preparação impede a entrada no bloco with. 4 min
  6. Reconhecer falhas na própria liberação Entenda o que acontece quando a tentativa de liberar um recurso também falha. 3 min
  7. Testar o contrato com um recurso controlado Execute uma suíte local com pytest para observar, de forma verificável, os caminhos de aquisição, uso, liberação e falha de um gerenciador de contexto. 3 min
  8. Corrigir e validar um gerenciador completo Corrija dois defeitos comuns em uma classe gerenciadora e valide, no seu computador, os caminhos de sucesso e falha. 4 min

O que você vai aprender

  • Implementar __enter__ e __exit__ para delimitar o uso de um recurso.
  • Distinguir o gerenciador do valor entregue ao nome após as.
  • Liberar recursos após saídas normais ou excepcionais do bloco.
  • Tratar falhas durante a entrada sem presumir que __exit__ será chamado.
  • Testar aquisição, liberação e propagação de exceções com um recurso controlado.

Antes de começar

  • Criar objetos com classes e métodos de instância
  • Tratar exceções com try, except, else e finally
  • Ler e gravar arquivos de texto com with
  • Isolar dependências com substitutos de teste

Passo 1 de 8

Delimitar a responsabilidade pelo recurso

Entenda o ciclo de vida que um gerenciador de contexto organiza antes de implementar seus métodos especiais.

Um escopo com responsável

O que o with delimita

Você já usou with com arquivos. De forma geral, ele delimita um escopo de uso: há uma entrada antes do bloco, o uso dentro dele e uma saída ao deixá-lo.

Um gerenciador de contexto é o objeto que define esse ciclo. Sua responsabilidade não é apenas fornecer algo ao bloco: ele também coordena a liberação do recurso quando aquele uso termina.

Ciclo de vida do recurso

O gerenciador fica responsável pelas bordas do escopo; o código do bloco usa o recurso apenas enquanto ele está disponível.

Diagrama mostrando um gerenciador coordenando as etapas configurar, adquirir, usar dentro de uma área delimitada e liberar ao sair dessa área.

Configurar → adquirir → usar no bloco → liberar. O bloco não assume a limpeza.

Exemplo condutor: recurso observável

Por que controlar o recurso?

Nos próximos passos, usaremos um recurso em memória que registra eventos e expõe se foi liberado. Isso torna o ciclo de vida verificável sem depender de um arquivo, conexão ou serviço externo.

A configuração do gerenciador informa como obter o recurso. A aquisição cria ou obtém o recurso. O bloco o utiliza. Ao final, o gerenciador deve solicitar a liberação de forma explícita — não esperar que referências ao objeto desapareçam.

Exemplo

Estados que queremos observar

Em um uso bem-sucedido, o registro poderá mostrar uma sequência como:

["adquirido", "usado", "liberado"]

E o recurso terminará com um estado equivalente a liberado = True.

A implementação dessa classe e dos métodos do protocolo virá nos próximos passos. Por enquanto, retenha a divisão de responsabilidades: o código no bloco usa; o gerenciador organiza a obtenção e a liberação.

Dica

Uma instância por uso

Adote a convenção deste tutorial: crie uma nova instância do gerenciador para cada uso de with. Assim, cada instância representa um ciclo de vida claro e independente.

Verifique o ciclo de vida

Ordene um uso bem-sucedido

Coloque as etapas na ordem esperada para um uso bem-sucedido de um recurso com gerenciador de contexto.

  1. Liberar o recurso ao sair do bloco
  2. Usar o recurso dentro do bloco
  3. Adquirir ou preparar o recurso
  4. Configurar uma nova instância do gerenciador

Quem libera?

No desenho deste tutorial, qual objeto deve assumir a responsabilidade pela liberação do recurso ao fim do escopo?

Passo 2 de 8

Implementar __enter__ e escolher o valor de as

Configure o gerenciador, adquira o recurso no momento certo e escolha conscientemente o objeto que o bloco receberá após as.

Configurar agora, adquirir ao entrar

Dois momentos diferentes

No desenho deste tutorial, __init__ apenas guarda a configuração necessária para obter o recurso. A aquisição acontece em __enter__, quando a execução efetivamente alcança o with.

Essa separação evita adquirir um recurso ao criar um gerenciador que talvez nunca seja usado. Cada uso cria uma nova instância do gerenciador.

Fluxo entre configuração e bloco

Observe que o nome depois de as recebe o retorno de __enter__, não necessariamente o próprio gerenciador.

Diagrama mostrando uma instância gerenciadora configurada, a chamada de __enter__ adquirindo um recurso controlado e o recurso sendo entregue à variável após as dentro do bloco with.

__init__ configura; __enter__ adquire; o retorno de __enter__ vai para as.

Um gerenciador que entrega o recurso

A fábrica torna o recurso controlável

A fábrica é um objeto chamável que cria o recurso. Recebê-la no construtor permite trocar a criação real por uma versão controlada em testes.

__enter__ guarda o recurso na instância gerenciadora e o devolve. Assim, o bloco usa o recurso, enquanto o gerenciador mantém a referência de que precisará na saída.

Classe inicial

Crie uma instância nova para cada with.

python
class RecursoControlado:
    def __init__(self, eventos):
        self.eventos = eventos
        self.liberado = False


class GerenciadorDeRecurso:
    def __init__(self, fabrica):
        self.fabrica = fabrica      # configuração; ainda não adquiriu
        self.recurso = None

    def __enter__(self):
        self.recurso = self.fabrica()  # aquisição ao entrar no with
        return self.recurso            # valor entregue após "as"

    def __exit__(self, exc_type, exc_value, traceback):
        # A saída será implementada no próximo passo.
        pass


eventos = []

def criar_recurso():
    eventos.append("adquirido")
    return RecursoControlado(eventos)

with GerenciadorDeRecurso(criar_recurso) as recurso:
    print(recurso is not None)  # True

print(eventos)  # ['adquirido']

O que cada nome referencia?

Relacione os elementos

Considere o código anterior. Relacione cada elemento à sua função ou identidade.

Toque em um item e depois no par correspondente.

Retornar self também é uma escolha

A interface do bloco é uma decisão

Se o bloco precisar das operações do próprio gerenciador, __enter__ pode retornar self. Mas, quando o objetivo é expor somente o recurso, retornar self.recurso cria uma interface mais direta.

Em ambos os casos, a responsabilidade pela saída permanece na instância gerenciadora: with chamará seu __exit__ depois de uma entrada bem-sucedida.

Preveja a identidade

Com a classe mostrada, qual afirmação é correta dentro deste bloco?

gerenciador = GerenciadorDeRecurso(criar_recurso)
with gerenciador as alvo:
    ...

Passo 3 de 8

Implementar a saída normal e excepcional

Faça o gerenciador liberar o recurso depois de uma entrada bem-sucedida e reconheça os dados recebidos em cada forma de saída do bloco.

A saída recebe o contexto da execução

O papel de __exit__

Depois que __enter__ termina com sucesso, o Python chama __exit__ ao sair do bloco with. Isso vale para o fim normal do bloco, para um return e para uma exceção que deixa o bloco.

A assinatura é __exit__(self, exc_type, exc_value, traceback). Os três últimos parâmetros descrevem uma exceção que está saindo do bloco — ou são None quando não há exceção.

Fluxo após uma entrada bem-sucedida

O retorno do bloco e a exceção percorrem caminhos diferentes, mas ambos passam por __exit__.

Diagrama mostrando __enter__ bem-sucedido, execução do bloco with e três saídas: término normal, return e exceção; as três convergem em __exit__. Nas duas primeiras, os argumentos de exceção são None; na última, contêm dados da exceção.

Após uma entrada bem-sucedida, a saída do bloco passa por exit.

Uma saída que sempre tenta liberar

O gerenciador guarda o recurso que foi obtido em __enter__.

python
class SessaoControlada:
    def __init__(self, fabrica):
        self.fabrica = fabrica
        self.recurso = None

    def __enter__(self):
        self.recurso = self.fabrica()
        return self.recurso

    def __exit__(self, exc_type, exc_value, traceback):
        self.recurso.liberar()
        return False

Saída normal e saída por return

Três None não significam apenas “fim da última linha”

Em uma saída sem exceção, exc_type, exc_value e traceback recebem None. Isso também ocorre quando um return dentro do with encerra a função: antes de a função entregar seu valor, o Python chama __exit__.

Exemplo

Return ainda libera antes de retornar

Considere que recurso.liberar() registra "liberado" em uma lista de eventos:

<code>def consultar(fabrica):
with SessaoControlada(fabrica) as recurso:
return recurso.valor
</code>

A ordem é: __enter__ obtém o recurso → o valor é preparado para retorno → __exit__(None, None, None) libera o recurso → a função entrega recurso.valor ao chamador.

O return não pula a etapa de saída.

Complete a chamada

Quando um return sai de um bloco with cuja entrada teve sucesso, exit recebe (___, ___, ___).

Exceção: liberar e deixar propagar

Dados da exceção e retorno explícito

Se uma exceção sai do bloco, exc_type recebe sua classe, exc_value recebe a instância e traceback recebe o objeto de traceback. Ainda assim, a primeira ação básica de __exit__ é liberar o recurso.

Ao retornar False, o método informa que não deve impedir a propagação dessa exceção. A decisão de retornar False torna essa intenção visível no código.

Registrar a saída sem alterar a exceção

Este exemplo registra se houve exceção, libera o recurso e preserva o fluxo original.

python
def __exit__(self, exc_type, exc_value, traceback):
    if exc_type is None:
        self.eventos.append("saida normal")
    else:
        self.eventos.append(f"saida com {exc_type.__name__}")

    self.recurso.liberar()
    return False

Preserve a exceção

Complete o retorno para liberar o recurso sem impedir que a exceção do bloco continue: return ___.

Dica

Chamada não é confirmação de sucesso

O protocolo garante a chamada de __exit__ depois de uma entrada bem-sucedida; ele não transforma a liberação em uma operação infalível. Portanto, registre ou atualize o estado de “liberado” apenas de acordo com o contrato do próprio recurso, não apenas porque a tentativa foi feita.

Passo 4 de 8

Controlar a propagação sem esconder erros

Escolha o retorno de exit de modo que exceções do bloco sejam preservadas por padrão e só sejam suprimidas quando isso fizer parte do contrato.

O retorno decide o destino da exceção

Propagar é o padrão seguro

Quando uma exceção sai do bloco with, Python chama __exit__ com os dados dessa exceção. O valor retornado por __exit__ decide se ela continua seu caminho:

  • Um valor falso — como False ou None — permite a propagação.
  • Um valor verdadeiro na avaliação booleana suprime a exceção.

Se não há uma regra explícita para absorver aquele erro, retorne False. Não é necessário usar raise dentro de __exit__ apenas para reenviar a exceção original.

Do bloco ao chamador

O retorno de __exit__ só influencia uma exceção que esteja saindo do bloco.

Diagrama de fluxo mostrando um bloco with que lança uma exceção, passa por __exit__, e segue ao chamador com retorno False ou é interrompida com retorno True.

False deixa a exceção sair; um valor verdadeiro a suprime.

Retorne uma decisão explícita

Uma saída que não esconde a falha

A limpeza é feita em todos os casos após uma entrada bem-sucedida. O False documenta que erros do bloco não serão absorvidos.

python
class Sessao:
    def __enter__(self):
        print("adquirir")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("liberar")
        return False

try:
    with Sessao():
        raise ValueError("dados inválidos")
except ValueError as erro:
    print(f"tratada fora do with: {erro}")

# adquirir
# liberar
# tratada fora do with: dados inválidos

Atenção

Cuidado com retornos incidentais

Evite retornar diretamente o resultado de uma operação de limpeza sem conhecer seu valor booleano. Se ela retornar um objeto verdadeiro, a exceção do bloco será suprimida por acidente.

Prefira executar a limpeza e retornar False explicitamente. A supressão deve ser uma decisão prevista e documentada pelo contrato do gerenciador.

Supressão muda o fluxo, não desfaz a falha

Quando suprimir é intencional

Um gerenciador pode optar por suprimir uma exceção específica, por exemplo, se ela for esperada pelo contrato. Nesse caso, a execução continua depois do with. A instrução que lançou a exceção não é retomada nem concluída.

Supressão deliberada de KeyError

Este exemplo é deliberadamente específico: apenas KeyError é suprimida. Outras exceções continuam a se propagar.

python
class IgnorarChaveAusente:
    def __enter__(self):
        return {}

    def __exit__(self, exc_type, exc_value, traceback):
        return exc_type is KeyError

with IgnorarChaveAusente() as dados:
    print("antes")
    print(dados["inexistente"])
    print("nunca aparece")

print("depois do with")

# antes
# depois do with

Dica

E se não ocorreu exceção?

Em uma saída normal, os argumentos de exceção são None. Nesse cenário, o retorno de __exit__ não suprime nada e não cancela um return executado dentro do bloco with.

Verifique a consequência do retorno

Preveja o fluxo

Qual é o resultado deste código?

class Monitor:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("saindo")
        # retorno implícito: None

with Monitor():
    raise ValueError("falhou")

print("continua")

Passo 5 de 8

Limpar aquisições parciais quando a entrada falha

Proteja o recurso adquirido em enter quando uma etapa posterior de preparação impede a entrada no bloco with.

A entrada só se completa após __enter__ retornar

Uma falha antes do bloco

O protocolo só considera a entrada bem-sucedida quando __enter__ termina e retorna um valor. Se ele lança uma exceção, o corpo do with não é executado e __exit__ não é chamado automaticamente.

Isso cria um caminho importante: a aquisição pode ter dado certo, mas uma preparação posterior pode falhar. Nesse caso, o próprio __enter__ deve liberar o que já adquiriu antes de deixar a exceção continuar.

Três estados possíveis da entrada

Diagrama de fluxo com três caminhos: falha antes de adquirir o recurso sem limpeza; aquisição seguida de falha de preparação com limpeza local; entrada concluída com execução do bloco e posterior saída por __exit__.

A limpeza local é necessária somente se o recurso já foi adquirido, mas a entrada ainda não foi concluída.

Atenção

Não espere por __exit__

Não coloque a limpeza dessa aquisição parcial em __exit__: como __enter__ falhou, não há garantia de uma chamada posterior a esse método. Também não tente liberar um recurso que não chegou a ser obtido.

Limpeza no mesmo método que adquiriu

O padrão de responsabilidade

Depois de adquirir o recurso, envolva apenas a preparação posterior em try. Se ela falhar, libere o recurso no except e use raise sem argumento para relançar a mesma exceção ativa.

O raise simples preserva o tipo, a instância e o traceback originais da falha de preparação.

Gerenciador com limpeza de aquisição parcial

python
class RecursoControlado:
    def __init__(self, eventos):
        self.eventos = eventos
        self.liberado = False

    def liberar(self):
        self.eventos.append("liberar")
        self.liberado = True


class Sessao:
    def __init__(self, fabrica, preparar):
        self.fabrica = fabrica
        self.preparar = preparar
        self.recurso = None

    def __enter__(self):
        self.recurso = self.fabrica()      # aquisição concluída
        try:
            self.preparar(self.recurso)    # pode falhar
        except:
            self.recurso.liberar()         # limpeza local
            raise                          # relança a falha original
        return self.recurso

    def __exit__(self, exc_type, exc_value, traceback):
        self.recurso.liberar()
        return False


eventos = []
recurso = RecursoControlado(eventos)

def fabrica():
    eventos.append("adquirir")
    return recurso

def preparar(recurso):
    eventos.append("preparar")
    raise RuntimeError("preparação falhou")

try:
    with Sessao(fabrica, preparar) as ativo:
        eventos.append("bloco")
except RuntimeError:
    eventos.append("erro observado")

print(eventos)
# ['adquirir', 'preparar', 'liberar', 'erro observado']

Dica

Escopo do try

Deixe a aquisição fora do try quando ela não exige limpeza caso falhe. Assim, a limpeza local só é executada depois que self.recurso recebeu um recurso válido.

Reconheça a sequência de falha

Ordene o caminho de aquisição parcial

Considere que fabrica() devolve o recurso, mas preparar(recurso) lança uma exceção. Coloque os eventos na ordem em que devem ocorrer.

  1. raise sem argumento relança a exceção de preparação.
  2. O except de __enter__ libera o recurso adquirido.
  3. A fábrica adquire e devolve o recurso.
  4. A preparação lança uma exceção.

Justifique a ausência de __exit__

Por que esse caminho não deve esperar que __exit__ libere o recurso? Explique também onde a liberação deve ocorrer.

Escreva pelo menos 80 caracteres (0/80).

Passo 6 de 8

Reconhecer falhas na própria liberação

Entenda o que acontece quando a tentativa de liberar um recurso também falha.

Limpeza foi tentada não significa limpeza concluída

O limite da garantia

Depois de uma entrada bem-sucedida, o protocolo chama __exit__. Isso garante uma tentativa de liberação, não que a operação de liberação terá sucesso.

Se recurso.fechar() lançar uma exceção dentro de __exit__, essa nova exceção chega ao chamador — mesmo que o bloco with tenha terminado normalmente. Portanto, um registro como "liberação iniciada" não permite afirmar que o recurso foi liberado.

Dois resultados possíveis da saída

A chamada de limpeza pode concluir ou falhar; os dois caminhos precisam ser tratados como resultados diferentes.

Diagrama de fluxo mostrando a saída de um bloco with, a chamada de __exit__ e dois desfechos: recurso liberado com sucesso ou exceção de liberação propagada ao chamador.

__exit__ participa da saída, mas não torna a operação de liberação infalível.

Atenção

Não declare sucesso antes da hora

Evite registrar ou expor que o recurso foi “liberado” antes de fechar() retornar sem exceção. A política deste exemplo é não esconder falhas de liberação: a exceção deve permanecer visível para quem chamou o código.

Quando o bloco e a limpeza falham

A falha da limpeza substitui a que estava saindo

Se o bloco lança uma exceção e __exit__ também lança outra durante a limpeza, a exceção da limpeza é a que chega diretamente ao chamador. A exceção original normalmente continua visível no encadeamento do traceback, como o contexto da falha posterior.

Não é necessário devolver um valor especial para produzir esse efeito: basta que a operação de liberação lance sua própria exceção.

Uma liberação que falha

Execute este exemplo localmente e observe o traceback completo.

python
class RecursoControlado:
    def __init__(self, eventos, falhar_ao_liberar=False):
        self.eventos = eventos
        self.falhar_ao_liberar = falhar_ao_liberar
        self.liberado = False

    def liberar(self):
        self.eventos.append("tentou liberar")
        if self.falhar_ao_liberar:
            raise RuntimeError("falha ao liberar")
        self.liberado = True
        self.eventos.append("liberou")


class Gerenciador:
    def __init__(self, recurso):
        self.recurso = recurso

    def __enter__(self):
        return self.recurso

    def __exit__(self, exc_type, exc_value, traceback):
        self.recurso.liberar()
        return False


eventos = []
recurso = RecursoControlado(eventos, falhar_ao_liberar=True)

try:
    with Gerenciador(recurso):
        eventos.append("bloco დაიწყო")
        raise ValueError("falha no bloco")
except RuntimeError as erro:
    print(type(erro).__name__, erro)

print(eventos)
print(recurso.liberado)

Exemplo

Resultado a interpretar

A captura recebe RuntimeError: falha ao liberar, e não o ValueError diretamente. A lista de eventos termina em ['bloco iniciou', 'tentou liberar'] se você usar esse texto no evento; no código acima, ela registra exatamente ['bloco iniciou', 'tentou liberar'] após corrigir o rótulo para português. Não aparece o evento "liberou", e recurso.liberado continua False.

No traceback, o ValueError do bloco aparece como contexto da falha de liberação.

Também vale para a limpeza em __enter__

Aquisição parcial: limpeza local também pode falhar

No caminho já estudado em que __enter__ adquire o recurso e uma preparação posterior falha, a limpeza é responsabilidade do próprio __enter__. Se essa limpeza lançar uma exceção, ela também substitui a falha de preparação que estava ativa.

Assim, documente o contrato de forma precisa: o gerenciador tenta liberar recursos adquiridos; ele não promete uma liberação que a operação subjacente não conseguiu concluir.

Diagnostique o cenário

No exemplo anterior, o bloco lança ValueError("falha no bloco") e liberar() lança RuntimeError("falha ao liberar"). Qual exceção o chamador observa diretamente? O que os eventos e o atributo liberado permitem concluir sobre a limpeza?

Escreva pelo menos 80 caracteres (0/80).

Passo 7 de 8

Testar o contrato com um recurso controlado

Execute uma suíte local com pytest para observar, de forma verificável, os caminhos de aquisição, uso, liberação e falha de um gerenciador de contexto.

A matriz de evidências

O que cada teste deve provar

Um recurso controlado torna visível o que normalmente ocorreria fora do programa: ele registra eventos, informa se foi liberado e pode falhar em pontos escolhidos. Assim, o teste verifica o contrato, não apenas o resultado final.

Em cada cenário, observe: o valor recebido por as, a ordem dos eventos, o número de liberações e a exceção que chega ao chamador.

Fluxos que a suíte vai observar

Diagrama de fluxos de um gerenciador de contexto: aquisição leva ao bloco e depois à liberação; falha antes da aquisição não chega ao bloco nem à liberação; falha de preparação após aquisição leva à limpeza local; falha de liberação sai como erro.

Depois de uma entrada bem-sucedida, a saída tenta liberar o recurso. Se __enter__ falhar, o bloco não começa e __exit__ não participa.

Relacionar cenário e evidência

Qual é a evidência decisiva?

Associe cada cenário à verificação mais importante.

Toque em um item e depois no par correspondente.

Criar o recurso e o gerenciador testáveis

No seu computador

Crie uma pasta vazia e salve o primeiro arquivo abaixo como contextos.py. A fábrica é injetada para que o teste escolha se a aquisição funciona ou falha. O recurso registra cada etapa em uma lista compartilhada.

contextos.py

python
class RecursoControlado:
    def __init__(self, eventos, falhar_ao_liberar=False):
        self.eventos = eventos
        self.falhar_ao_liberar = falhar_ao_liberar
        self.liberado = False

    def liberar(self):
        self.eventos.append("liberar")
        if self.falhar_ao_liberar:
            raise RuntimeError("falha na liberacao")
        self.liberado = True


class GerenciadorRecurso:
    def __init__(self, fabrica, preparar=lambda recurso: None):
        self._fabrica = fabrica
        self._preparar = preparar
        self._recurso = None

    def __enter__(self):
        recurso = None
        try:
            recurso = self._fabrica()
            self._recurso = recurso
            self._preparar(recurso)
        except:
            if recurso is not None:
                recurso.liberar()
            raise
        return recurso

    def __exit__(self, exc_type, exc_value, traceback):
        self._recurso.liberar()
        return False

Executar a suíte e interpretar o resultado

test_contextos.py

Salve este segundo arquivo na mesma pasta. Ele cobre a matriz de cenários sem depender de arquivos ou serviços externos.

python
import pytest

from contextos import GerenciadorRecurso, RecursoControlado


def criar_fabrica(eventos, caixa, *, falhar=False, falhar_ao_liberar=False):
    def fabrica():
        eventos.append("adquirir")
        if falhar:
            raise ConnectionError("falha na aquisicao")
        recurso = RecursoControlado(eventos, falhar_ao_liberar)
        caixa["recurso"] = recurso
        return recurso
    return fabrica


def test_termino_normal_entrega_recurso_e_libera_uma_vez():
    eventos, caixa = [], {}
    fabrica = criar_fabrica(eventos, caixa)

    with GerenciadorRecurso(fabrica) as recebido:
        eventos.append("bloco")
        assert recebido is caixa["recurso"]

    assert eventos == ["adquirir", "bloco", "liberar"]
    assert caixa["recurso"].liberado is True


def test_return_ainda_chama_a_saida():
    eventos, caixa = [], {}

    def usar():
        with GerenciadorRecurso(criar_fabrica(eventos, caixa)):
            eventos.append("bloco")
            return "resultado"

    assert usar() == "resultado"
    assert eventos == ["adquirir", "bloco", "liberar"]


def test_excecao_do_bloco_propaga_e_recurso_e_liberado():
    eventos, caixa = [], {}

    with pytest.raises(ValueError, match="bloco"):
        with GerenciadorRecurso(criar_fabrica(eventos, caixa)):
            eventos.append("bloco")
            raise ValueError("erro no bloco")

    assert eventos == ["adquirir", "bloco", "liberar"]
    assert caixa["recurso"].liberado is True


def test_falha_antes_da_aquisicao_nao_executa_bloco_nem_saida():
    eventos, caixa = [], {}

    with pytest.raises(ConnectionError, match="aquisicao"):
        with GerenciadorRecurso(criar_fabrica(eventos, caixa, falhar=True)):
            eventos.append("bloco")

    assert eventos == ["adquirir"]
    assert caixa == {}


def test_falha_de_preparacao_limpa_uma_vez_sem_entrar_no_bloco():
    eventos, caixa = [], {}

    def preparar(recurso):
        eventos.append("preparar")
        raise LookupError("falha na preparacao")

    with pytest.raises(LookupError, match="preparacao"):
        with GerenciadorRecurso(criar_fabrica(eventos, caixa), preparar):
            eventos.append("bloco")

    assert eventos == ["adquirir", "preparar", "liberar"]
    assert caixa["recurso"].liberado is True


def test_falha_na_liberacao_substitui_falha_do_bloco():
    eventos, caixa = [], {}
    fabrica = criar_fabrica(eventos, caixa, falhar_ao_liberar=True)

    with pytest.raises(RuntimeError, match="liberacao") as saida:
        with GerenciadorRecurso(fabrica):
            eventos.append("bloco")
            raise ValueError("erro original do bloco")

    assert eventos == ["adquirir", "bloco", "liberar"]
    assert isinstance(saida.value.__context__, ValueError)
    assert caixa["recurso"].liberado is False

Dica

Execute localmente

No terminal aberto nessa pasta, execute:

python -m pytest -q

O resultado esperado é 6 passed. A última verificação não afirma que a liberação teve sucesso: ela confirma que a tentativa foi registrada e que a falha de liberação foi propagada.

Relate duas evidências da sua execução

Depois de executar a suíte, relate uma evidência de liberação e uma evidência de propagação de exceção. Inclua os nomes dos eventos ou das exceções que você observou.

Escreva pelo menos 80 caracteres (0/80).

Passo 8 de 8

Corrigir e validar um gerenciador completo

Corrija dois defeitos comuns em uma classe gerenciadora e valide, no seu computador, os caminhos de sucesso e falha.

Localize os dois defeitos

Uma classe quase correta

Considere esta primeira versão. Ela adquire um recurso, prepara-o e o entrega ao bloco. Porém, há dois defeitos: se prepare() falhar, o recurso já obtido não será liberado; e o retorno de release() pode ser verdadeiro, fazendo __exit__ suprimir sem querer uma exceção do bloco.

Versão com defeitos intencionais

python
class GerenciadorRecurso:
    def __init__(self, factory):
        self._factory = factory
        self._resource = None

    def __enter__(self):
        self._resource = self._factory()
        self._resource.prepare()
        return self._resource

    def __exit__(self, exc_type, exc_value, traceback):
        return self._resource.release()

Dois caminhos que exigem decisões distintas

Diagrama de fluxo mostrando aquisição seguida de preparação; uma falha durante a preparação exige limpeza local, enquanto uma entrada concluída leva o bloco ao encerramento por __exit__.

A limpeza local cobre a falha antes de __enter__ retornar; __exit__ cobre somente uma entrada bem-sucedida.

Aplique a correção

Contrato adotado

A fábrica adquire o recurso. Se a preparação falhar, __enter__ libera localmente o que já adquiriu e deixa a exceção sair com raise. Depois que __enter__ retorna, __exit__ é chamado na saída do bloco: ele tenta liberar e retorna explicitamente False, preservando qualquer exceção do bloco. Se a própria liberação falhar, essa nova falha também deve aparecer ao chamador.

gerenciador.py — versão corrigida

Crie um arquivo chamado gerenciador.py com este conteúdo.

python
class GerenciadorRecurso:
    def __init__(self, factory):
        self._factory = factory
        self._resource = None
        self.exit_calls = 0

    def __enter__(self):
        self._resource = self._factory()
        try:
            self._resource.prepare()
        except Exception:
            # __exit__ não será chamado se a entrada falhar.
            self._resource.release()
            raise
        return self._resource

    def __exit__(self, exc_type, exc_value, traceback):
        self.exit_calls += 1
        self._resource.release()
        return False

Dica

Não retorne a limpeza

Mesmo que release() devolva um valor verdadeiro, não o repasse como retorno de __exit__. O retorno de __exit__ controla a supressão da exceção do bloco; por isso, use False de forma explícita quando a política for propagar.

Execute a validação local

Teste todos os caminhos relevantes

No mesmo diretório, crie test_gerenciador.py. O recurso controlado registra eventos e permite configurar falhas. Depois execute python -m pytest -q. Todos os seis testes devem passar.

test_gerenciador.py

python
import pytest

from gerenciador import GerenciadorRecurso


class RecursoControlado:
    def __init__(self, events, fail_prepare=False, fail_release=False):
        self.events = events
        self.fail_prepare = fail_prepare
        self.fail_release = fail_release
        self.released = False

    def prepare(self):
        self.events.append("preparar")
        if self.fail_prepare:
            raise RuntimeError("falha ao preparar")

    def release(self):
        self.events.append("tentar_liberar")
        if self.fail_release:
            raise RuntimeError("falha ao liberar")
        self.released = True
        self.events.append("liberado")
        return True


def fabrica(events, **falhas):
    recurso = RecursoControlado(events, **falhas)

    def criar():
        events.append("adquirido")
        return recurso

    return criar, recurso


def test_entrega_o_recurso_e_libera_na_saida_normal():
    events = []
    criar, recurso = fabrica(events)
    gerenciador = GerenciadorRecurso(criar)

    with gerenciador as recebido:
        assert recebido is recurso
        events.append("usar")

    assert recurso.released is True
    assert events == ["adquirido", "preparar", "usar", "tentar_liberar", "liberado"]


def test_saida_por_return_tambem_libera():
    events = []
    criar, recurso = fabrica(events)

    def usar():
        with GerenciadorRecurso(criar) as recebido:
            return recebido

    assert usar() is recurso
    assert recurso.released is True


def test_excecao_do_bloco_nao_e_suprimida():
    events = []
    criar, recurso = fabrica(events)

    with pytest.raises(ValueError, match="erro no bloco"):
        with GerenciadorRecurso(criar):
            raise ValueError("erro no bloco")

    assert recurso.released is True


def test_falha_antes_da_aquisicao_nao_chama_exit():
    def criar():
        raise RuntimeError("falha ao adquirir")

    gerenciador = GerenciadorRecurso(criar)

    with pytest.raises(RuntimeError, match="falha ao adquirir"):
        with gerenciador:
            pytest.fail("o bloco não deveria executar")

    assert gerenciador.exit_calls == 0


def test_falha_apos_aquisicao_libera_localmente_sem_chamar_exit():
    events = []
    criar, recurso = fabrica(events, fail_prepare=True)
    gerenciador = GerenciadorRecurso(criar)

    with pytest.raises(RuntimeError, match="falha ao preparar"):
        with gerenciador:
            pytest.fail("o bloco não deveria executar")

    assert recurso.released is True
    assert gerenciador.exit_calls == 0
    assert events == ["adquirido", "preparar", "tentar_liberar", "liberado"]


def test_falha_na_liberacao_substitui_a_falha_do_bloco():
    events = []
    criar, recurso = fabrica(events, fail_release=True)

    with pytest.raises(RuntimeError, match="falha ao liberar") as capturada:
        with GerenciadorRecurso(criar):
            raise ValueError("erro no bloco")

    assert isinstance(capturada.value.__context__, ValueError)
    assert recurso.released is False

Registre sua execução

Após executar os testes, descreva o resultado. Explique como a alteração em __enter__ evita o vazamento e como o return False evita a supressão acidental.

Escreva pelo menos 100 caracteres (0/100).

Revise o contrato antes de reutilizá-lo

Resumo

Critérios para revisar um gerenciador com classe

  • Defina claramente quem adquire o recurso e mantenha a instância gerenciadora responsável pela saída.
  • Faça __enter__ retornar exatamente o valor que o bloco deve usar após as; esse valor pode ser diferente do gerenciador.
  • Se uma etapa posterior à aquisição falhar dentro de __enter__, libere localmente o recurso já obtido e use raise para preservar a falha ativa.
  • Após uma entrada bem-sucedida, libere em __exit__ tanto na saída normal quanto na excepcional e retorne False ou None quando não quiser suprimir a exceção.
  • Trate a limpeza como uma tentativa que pode falhar: chamar release() não comprova que ela teve sucesso; documente qual falha será propagada.

Justificativa final

Justifique o contrato de falhas desta classe: quem adquire, quem libera, quando __exit__ participa e qual comportamento ocorre se a liberação falhar.

Escreva pelo menos 180 caracteres (0/180).

Tutorial concluído

Parabéns! Você concluiu: Criar gerenciadores de contexto com classes

Você corrigiu e validou um gerenciador de contexto com classe, incluindo aquisição parcial, saída excepcional e falhas de liberação. No próximo tutorial, você verá uma alternativa baseada em contextlib.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso