Trilha de aprendizado · Nível 12 · Tutorial 8

Definir interfaces estruturais com Protocol

Formalizar contratos de colaboradores com Protocol e verificar implementações independentes sem exigir uma classe base compartilhada.

  • Nível: Avançado
  • Duração: 22 min
  • 8 passos
Definir interfaces estruturais com Protocol

O que você vai percorrer

  1. Partir das necessidades do consumidor Descubra o contrato mínimo observando o que o código consumidor realmente pede a seus colaboradores. 2 min
  2. Declarar as operações com Protocol Declare um contrato estrutural mínimo para destinos que recebem mensagens, usando assinaturas anotadas e sem criar uma implementação. 3 min
  3. Verificar classes no ponto de consumo Use o protocolo onde o colaborador é recebido ou armazenado para que o mypy verifique a compatibilidade estrutural. 3 min
  4. Corrigir divergências de assinatura Use os diagnósticos do mypy para localizar e corrigir incompatibilidades entre as chamadas permitidas por um Protocol e os métodos oferecidos por implementações independentes. 4 min
  5. Entender a exigência de atributos graváveis Declare atributos em um Protocol e reconheça as obrigações de leitura e escrita que eles criam para cada implementação. 3 min
  6. Exigir apenas leitura com property Declare uma consulta somente de leitura no protocolo e preserve a liberdade de cada implementação para armazenar ou calcular o valor. 3 min
  7. Aplicar o contrato à implementação e ao substituto de teste Use o mesmo protocolo para uma implementação de console e um substituto que registra chamadas durante testes. 3 min
  8. Consolidar uma interface estrutural verificável Aplique o fluxo completo de Protocol em um caso integrado: contrato mínimo, assinaturas compatíveis, acesso somente de leitura e teste comportamental. 3 min

O que você vai aprender

  • Declarar um Protocol com métodos e atributos necessários ao consumidor.
  • Verificar a compatibilidade de classes que não herdam explicitamente do protocolo.
  • Corrigir divergências de assinatura identificadas pelo mypy.
  • Escolher entre atributos graváveis e propriedades somente de leitura no contrato.

Antes de começar

  • Definir classes genéricas com parâmetros de tipo
  • Anotar funções recebidas e retornadas com Callable
  • Aplicar polimorfismo pelo comportamento dos objetos
  • Isolar dependências com substitutos de teste

Passo 1 de 8

Partir das necessidades do consumidor

Descubra o contrato mínimo observando o que o código consumidor realmente pede a seus colaboradores.

Comece pelo consumidor

O contrato nasce do uso

Ao definir uma interface, comece pelo objeto que depende de outro: o consumidor. O contrato mínimo contém apenas as operações que esse consumidor realmente usa.

No caso condutor, ServicoDeAvisos recebe uma mensagem e a encaminha a um destino. Por enquanto, sua única necessidade é enviar essa mensagem.

Consumidor do caso

python
class ServicoDeAvisos:
    def __init__(self, destino) -> None:
        self.destino = destino

    def avisar(self, mensagem: str) -> None:
        self.destino.enviar(mensagem)

Visualize a fronteira do contrato

O que atravessa a fronteira

O serviço conhece apenas a operação de que precisa. As diferentes formas de entregar a mensagem permanecem atrás dessa fronteira.

Diagrama mostrando ServicoDeAvisos chamando enviar(mensagem) em um contrato central, ligado a dois destinos independentes: um console e um sistema de e-mail. Detalhes internos aparecem apenas nos destinos.

O contrato mínimo, neste momento, é a capacidade de receber enviar(mensagem).

Dica

Regra prática

Não descreva o que uma implementação poderia fazer. Descreva somente o que o consumidor precisa chamar. Um contrato menor reduz acoplamento e amplia as opções de implementação.

Detalhes não entram por conveniência

Duas implementações possíveis

python
class DestinoConsole:
    def enviar(self, mensagem: str) -> None:
        print(mensagem)

    def configurar_cores(self, habilitadas: bool) -> None:
        ...


class DestinoEmail:
    def enviar(self, mensagem: str) -> None:
        print(f"E-mail enviado: {mensagem}")

    def reconectar_servidor(self) -> None:
        ...

As classes não precisam pertencer à mesma hierarquia para colaborar com o serviço. Ambas oferecem enviar, mas seus métodos adicionais são detalhes próprios.

ServicoDeAvisos não chama configurar_cores nem reconectar_servidor; portanto, essas operações não fazem parte de seu contrato.

Selecione o contrato mínimo

Qual operação é necessária?

Considerando exclusivamente o código de ServicoDeAvisos, qual deve ser a exigência mínima para um destino?

Passo 2 de 8

Declarar as operações com Protocol

Declare um contrato estrutural mínimo para destinos que recebem mensagens, usando assinaturas anotadas e sem criar uma implementação.

Do uso ao contrato

O contrato do destino

No passo anterior, o serviço consumidor precisava apenas pedir que um destino enviasse uma mensagem. Agora vamos registrar essa necessidade em um contrato estrutural: qualquer objeto que ofereça a operação esperada poderá ser usado mais adiante.

Importe Protocol de typing e crie uma classe que herda dele. O nome descreve o papel exigido pelo consumidor, não uma implementação específica.

Papel do Protocol

O protocolo fica entre quem consome a operação e quem a executa.

Diagrama mostrando um serviço de mensagens apontando para um contrato de envio, que por sua vez se relaciona a destinos independentes como console, e-mail e registro em memória.

O consumidor depende do contrato mínimo; as classes concretas ficam separadas dele.

Primeira declaração

Crie o contrato em um arquivo Python:

python
from typing import Protocol


class DestinoMensagem(Protocol):
    ...

Descrever a operação exigida

Assinatura, não execução

Dentro do protocolo, declare o método que o consumidor pode chamar. Os tipos do parâmetro e do retorno fazem parte do contrato. Como este destino apenas realiza o envio, ele retorna None.

As reticências (...) substituem o corpo: elas declaram a operação, mas não dizem como enviar a mensagem.

Contrato mínimo de envio

O contrato completo, por enquanto, tem uma única operação:

python
from typing import Protocol


class DestinoMensagem(Protocol):
    def enviar(self, mensagem: str) -> None:
        ...

Exemplo

O que este código afirma

DestinoMensagem afirma que o consumidor poderá chamar enviar com uma str e não receberá um valor de volta. Ele não imprime nada, não envia e-mail e não armazena mensagens: essas ações pertencem a classes concretas, declaradas separadamente.

Contrato estático, não validação em execução

Atenção

O Protocol não testa o objeto sozinho

As anotações permitem que um verificador estático, como o mypy, analise o código. Definir DestinoMensagem não faz Python validar objetos automaticamente quando o programa está em execução e não cria uma implementação para enviar.

Próximo uso

No próximo passo, o consumidor será anotado com esse protocolo. Nesse ponto, o mypy poderá verificar se um objeto independente oferece a operação exigida — sem que a classe precise herdar explicitamente de DestinoMensagem.

Prática: complete a declaração

Escolha a base da classe

Complete a declaração:

from typing import Protocol

class DestinoMensagem(____):
    def enviar(self, mensagem: str) -> None:
        ...

Declare sem implementar

Complete o corpo do método no protocolo:

class DestinoMensagem(Protocol):
    def enviar(self, mensagem: str) -> None:
        ____

Passo 3 de 8

Verificar classes no ponto de consumo

Use o protocolo onde o colaborador é recebido ou armazenado para que o mypy verifique a compatibilidade estrutural.

O contrato entra no consumidor

Anote o que o consumidor precisa

Depois de declarar um Protocol, use-o no ponto em que um objeto é recebido. Assim, o serviço depende do contrato DestinoMensagem, e não de uma classe concreta.

O mypy verifica cada valor passado para encaminhar: ele precisa oferecer enviar(mensagem: str) -> None. Não precisa herdar do protocolo.

Parâmetro tipado pelo protocolo

python
from typing import Protocol


class DestinoMensagem(Protocol):
    def enviar(self, mensagem: str) -> None: ...


def encaminhar(destino: DestinoMensagem, texto: str) -> None:
    destino.enviar(texto)


class Console:
    def enviar(self, mensagem: str) -> None:
        print(mensagem)


encaminhar(Console(), "Pedido recebido")

O ponto de verificação

Diagrama mostrando o serviço encaminhar recebendo um objeto Console por meio do contrato DestinoMensagem, com uma seta de verificação estática do mypy no argumento.

A anotação do parâmetro estabelece o contrato usado pelo consumidor; Console é compatível pela estrutura, sem herança.

Armazenar o colaborador pelo contrato

Variáveis e atributos também são pontos de controle

Anote uma variável quando ela guardar um colaborador e um atributo quando a classe mantiver essa dependência. Em cada atribuição, o mypy confere se o valor é compatível com o protocolo.

Dentro de ServicoAvisos, o atributo tem tipo DestinoMensagem. Portanto, o código do serviço só pode usar membros prometidos por esse contrato.

Atributo e variável anotados

python
class ServicoAvisos:
    def __init__(self, destino: DestinoMensagem) -> None:
        self.destino: DestinoMensagem = destino

    def avisar(self, texto: str) -> None:
        self.destino.enviar(texto)


class Email:
    def enviar(self, mensagem: str) -> None:
        print(f"E-mail: {mensagem}")

    def conectar_smtp(self) -> None:
        print("Conectando...")


principal: DestinoMensagem = Console()
servico = ServicoAvisos(Email())
servico.avisar("Pagamento aprovado")

Estrutura suficiente, extras ocultos

Membros extras não ampliam o contrato

Email tem conectar_smtp, mas esse método não está no protocolo. A compatibilidade permite usar Email como destino porque ele fornece enviar; porém, dentro de ServicoAvisos, self.destino.conectar_smtp() é um erro de tipo.

Isso preserva o desacoplamento: o consumidor continua válido para qualquer implementação que cumpra o contrato mínimo.

Associe o local à verificação

Relacione cada trecho ao que o mypy verifica.

Toque em um item e depois no par correspondente.

Passo 4 de 8

Corrigir divergências de assinatura

Use os diagnósticos do mypy para localizar e corrigir incompatibilidades entre as chamadas permitidas por um Protocol e os métodos oferecidos por implementações independentes.

O nome do método não basta

Compatibilidade é sobre chamadas possíveis

Uma classe pode ter um método chamado enviar e ainda assim não cumprir o protocolo. A implementação precisa aceitar todas as entradas que o consumidor pode fazer pelo contrato e devolver um resultado compatível.

O mypy compara a assinatura usada no ponto de consumo com a assinatura concreta. Assim, ele pode apontar erro mesmo quando o método existe.

Contrato e implementação precisam se alinhar

Compare as chamadas que o protocolo autoriza com as que a implementação realmente aceita.

Diagrama mostrando um contrato de envio com slots para mensagem, opção urgente e retorno booleano conectado a uma implementação com slots desalinhados.

Se um slot aceito pelo contrato falta, muda de forma ou produz outro tipo de retorno, a implementação não é substituível.

Exemplo

Um método existente, mas incompatível

from typing import Protocol

class Destino(Protocol):
    def enviar(self, mensagem: str, *, urgente: bool = False) -> bool: ...

class Console:
    def enviar(self, texto: str, urgente: bool) -> None:
        print(texto)

def encaminhar(destino: Destino, mensagem: str) -> bool:
    return destino.enviar(mensagem=mensagem)

encaminhar(Console(), "Relatório pronto")  # erro do mypy

Console.enviar diverge em três pontos: o parâmetro se chama texto, urgente virou obrigatório e o retorno é None, não bool.

Leia o diagnóstico pela chamada permitida

O contrato define o mínimo que a implementação deve suportar

Como o consumidor pode chamar enviar(mensagem=...), o nome mensagem faz parte da compatibilidade. Como ele pode omitir urgente, a implementação não pode exigir esse argumento.

Além disso, se o consumidor recebe um bool, a implementação deve produzir um bool. Não enfraqueça o protocolo nem use Any para esconder a divergência: corrija a assinatura que não atende ao contrato.

Correção da implementação

A assinatura abaixo preserva exatamente as necessidades do consumidor.

python
from typing import Protocol

class Destino(Protocol):
    def enviar(self, mensagem: str, *, urgente: bool = False) -> bool: ...

class Console:
    def enviar(self, mensagem: str, *, urgente: bool = False) -> bool:
        prefixo = "[URGENTE] " if urgente else ""
        print(f"{prefixo}{mensagem}")
        return True

def encaminhar(destino: Destino, mensagem: str) -> bool:
    return destino.enviar(mensagem=mensagem)

encaminhar(Console(), "Relatório pronto")
# mypy aceita Console como Destino, sem herança explícita.

Dica

Compare no sentido do consumidor

Pergunte: “Toda chamada válida para o Protocol também funciona nesta implementação?”. Parâmetros obrigatórios extras e parâmetros com nomes incompatíveis para chamadas nomeadas fazem a resposta ser não.

Associe cada divergência ao risco

Diagnóstico de assinaturas

Associe cada trecho de implementação ao motivo pelo qual ele não atende ao contrato enviar(self, mensagem: str, *, urgente: bool = False) -> bool.

Toque em um item e depois no par correspondente.

Corrija sem alterar o contrato

Proponha a assinatura correta

Uma implementação foi escrita assim:

class Arquivo:
    def enviar(self, mensagem: str, prioridade: int) -> None:
        print(mensagem)

Sem mudar o Protocol enviar(self, mensagem: str, *, urgente: bool = False) -> bool, escreva uma assinatura compatível para Arquivo.enviar e explique por que prioridade não pode continuar como parâmetro obrigatório.

Escreva pelo menos 80 caracteres (0/80).

Passo 5 de 8

Entender a exigência de atributos graváveis

Declare atributos em um Protocol e reconheça as obrigações de leitura e escrita que eles criam para cada implementação.

Um atributo no contrato pode ser lido e alterado

O consumidor determina a exigência

Além de enviar mensagens, o serviço agora precisa consultar e alterar o identificador do destino. Ao declarar destino: object no corpo do Protocol, você afirma que qualquer colaborador compatível deve expor um atributo que possa ser lido e também receber atribuições de valores do tipo object.

Essa exigência é mais forte do que apenas ter um atributo com esse nome: o consumidor pode executar tanto self._destino.destino quanto self._destino.destino = novo_destino.

Leitura e escrita pelo consumidor

O mesmo atributo participa de duas operações permitidas pelo contrato.

Diagrama mostrando um serviço de mensagens consultando e atualizando um campo de destino em um colaborador.

Um atributo declarado diretamente no protocolo precisa suportar a leitura e a escrita usadas pelo consumidor.

Contrato e consumidor

O tipo object permite ao consumidor atribuir qualquer objeto ao identificador do destino.

python
from typing import Protocol


class DestinoDeMensagens(Protocol):
    destino: object

    def enviar(self, mensagem: str) -> None: ...


class Notificador:
    def __init__(self, destino: DestinoDeMensagens) -> None:
        self._destino = destino

    def identificar_destino(self) -> object:
        return self._destino.destino

    def redirecionar(self, novo_destino: object) -> None:
        self._destino.destino = novo_destino

    def avisar(self, mensagem: str) -> None:
        self._destino.enviar(mensagem)

Por que um membro parecido ainda pode falhar

A escrita torna o atributo invariante

Se o contrato permite escrever qualquer object, uma implementação com destino: str não é segura. O Notificador poderia receber, por exemplo, um objeto de configuração e atribuí-lo ao campo; isso violaria a promessa da implementação de armazenar apenas str.

Do mesmo modo, uma propriedade sem setter pode ser lida, mas não aceita a atribuição que o consumidor está autorizado a fazer. Portanto, ela não satisfaz este contrato gravável.

Implementações incompatíveis

O mypy aponta o problema quando uma dessas instâncias é usada onde DestinoDeMensagens é esperado.

python
class CanalTexto:
    def __init__(self, destino: str) -> None:
        self.destino = destino

    def enviar(self, mensagem: str) -> None:
        print(f"{self.destino}: {mensagem}")


class CanalSemSetter:
    def __init__(self, destino: object) -> None:
        self._destino = destino

    @property
    def destino(self) -> object:
        return self._destino

    def enviar(self, mensagem: str) -> None:
        print(mensagem)


# Ambos são incompatíveis com o parâmetro de Notificador:
# Notificador(CanalTexto("console"))
# Notificador(CanalSemSetter("console"))

Exemplo

Correção: manter a capacidade de escrita

Uma implementação compatível pode armazenar object no atributo e oferecer o método exigido:

class CanalConfiguravel:
    def __init__(self, destino: object) -> None:
        self.destino = destino

    def enviar(self, mensagem: str) -> None:
        print(f"{self.destino}: {mensagem}")

notificador = Notificador(CanalConfiguravel("console"))
notificador.redirecionar({"fila": "avisos"})

Aqui, a atribuição de um dicionário é permitida pelo contrato e pela implementação.

Atenção

Compatibilidade não valida valores

O Protocol e o mypy verificam se as operações são compatíveis estaticamente. Eles não verificam, durante a execução, se um objeto escolhido como destino faz sentido para a regra de negócio.

Verifique a capacidade exigida

Qual implementação é compatível?

Considerando o protocolo DestinoDeMensagens com destino: object e enviar(self, mensagem: str) -> None, qual classe pode ser passada para Notificador?

Passo 6 de 8

Exigir apenas leitura com property

Declare uma consulta somente de leitura no protocolo e preserve a liberdade de cada implementação para armazenar ou calcular o valor.

Consultar não é poder alterar

Um contrato mais restrito

No step anterior, nome: str no corpo de um Protocol exigia leitura e escrita: o consumidor poderia fazer destino.nome = "novo".

Se o consumidor só precisa consultar o nome para exibi-lo, declare uma propriedade sem setter. Assim, a interface oferece leitura, mas não autoriza atribuições por meio da referência tipada pelo protocolo.

Leitura pelo contrato

A propriedade delimita o que o consumidor pode fazer, independentemente das capacidades do objeto concreto.

Diagrama mostrando um consumidor consultando o nome de dois destinos diferentes por uma interface de leitura, com a operação de escrita bloqueada apenas no caminho da interface.

O consumidor lê nome; ele não recebe permissão para alterá-lo pelo contrato.

Declarar a propriedade no Protocol

Contrato de consulta para o destino

A reticência declara a operação exigida, sem implementar seu corpo.

python
from typing import Literal, Protocol


class DestinoDeMensagem(Protocol):
    @property
    def nome(self) -> str: ...

    def enviar(self, mensagem: str) -> None: ...


class DestinoConsole:
    def __init__(self, nome: str) -> None:
        self.nome = nome  # atributo comum e mutável no objeto concreto

    def enviar(self, mensagem: str) -> None:
        print(f"[{self.nome}] {mensagem}")


class DestinoAuditoria:
    @property
    def nome(self) -> Literal["auditoria"]:
        return "auditoria"

    def enviar(self, mensagem: str) -> None:
        print(f"AUDIT: {mensagem}")


def rotulo(destino: DestinoDeMensagem) -> str:
    return f"Destino: {destino.nome}"


console = DestinoConsole("terminal")
print(rotulo(console))
console.nome = "terminal-secundario"  # permitido: console é concreto

contrato: DestinoDeMensagem = console
# contrato.nome = "outro"  # erro do mypy: a propriedade do Protocol não tem setter

O que a ausência do setter significa

Restrição no ponto de consumo

Se uma classe compatível possui um atributo nome mutável, esse atributo se torna imutável no objeto concreto quando ele é usado por um protocolo com @property sem setter.

Escolher o contrato adequado

Decisão de interface

Um serviço só exibe o nome do destino antes de enviar uma mensagem; ele nunca renomeia esse destino. Qual declaração deve entrar no Protocol para nome? Explique também por que uma implementação com atributo nome mutável ainda pode ser compatível, sem liberar atribuição pelo contrato.

Escreva pelo menos 120 caracteres (0/120).

Passo 7 de 8

Aplicar o contrato à implementação e ao substituto de teste

Use o mesmo protocolo para uma implementação de console e um substituto que registra chamadas durante testes.

Um contrato, dois destinos

O consumidor depende do contrato

O serviço de mensagens conhece apenas o contrato Destino: ele consulta identificador e chama enviar(). Por isso, tanto um destino que imprime no console quanto outro que guarda mensagens em memória podem ser usados sem herdar de Destino.

A implementação em memória possui mensagens, mas esse detalhe é útil somente ao teste. Ele não pertence ao protocolo porque o consumidor não precisa dele.

Contrato estrutural e evidências

O diagrama separa o que o mypy verifica do que o teste verifica.

Diagrama mostrando um serviço de mensagens ligado por um contrato Destino a ConsoleDestino e DestinoMemoria; o substituto em memória também se liga a um teste que inspeciona uma lista de mensagens.

O protocolo limita o serviço a identificador e enviar(). O teste conserva a referência concreta para consultar mensagens.

Implementações independentes

mensagens.py

Crie este arquivo completo. A anotação Destino no construtor é o ponto de consumo comum.

python
from typing import Protocol


class Destino(Protocol):
    @property
    def identificador(self) -> str: ...

    def enviar(self, mensagem: str) -> None: ...


class ConsoleDestino:
    def __init__(self, identificador: str) -> None:
        self._identificador = identificador

    @property
    def identificador(self) -> str:
        return self._identificador

    def enviar(self, mensagem: str) -> None:
        print(f"[{self.identificador}] {mensagem}")


class DestinoMemoria:
    def __init__(self, identificador: str) -> None:
        self.identificador = identificador
        self.mensagens: list[str] = []

    def enviar(self, mensagem: str) -> None:
        self.mensagens.append(mensagem)


class ServicoMensagens:
    def __init__(self, destino: Destino) -> None:
        self._destino = destino

    def encaminhar(self, mensagem: str) -> None:
        self._destino.enviar(mensagem)


# Esta atribuição faz o mypy verificar ConsoleDestino contra Destino.
destino_console: Destino = ConsoleDestino("console")
servico_console = ServicoMensagens(destino_console)

Teste comportamental e verificação estática

test_mensagens.py

Crie este segundo arquivo na mesma pasta. O teste passa o substituto ao serviço, mas mantém destino com seu tipo concreto para inspecionar os registros.

python
from mensagens import DestinoMemoria, ServicoMensagens


def test_encaminha_para_o_destino_em_memoria() -> None:
    destino = DestinoMemoria("teste")
    servico = ServicoMensagens(destino)

    servico.encaminhar("Pedido recebido")

    assert destino.identificador == "teste"
    assert destino.mensagens == ["Pedido recebido"]

Execute as duas verificações

No terminal, dentro da pasta dos arquivos, execute:

python -m mypy --strict mensagens.py test_mensagens.py
python -m pytest -q

O mypy verifica se as classes e as chamadas usadas satisfazem as anotações. O pytest executa o cenário e comprova que, neste caso, a mensagem foi registrada.

Atenção

Compatível não significa comprovadamente correto

O mypy não executa enviar() nem confirma que a mensagem foi armazenada. Por outro lado, um teste que passa não substitui a análise estática de outros usos possíveis do contrato. Use as duas evidências: compatibilidade estrutural e comportamento observado.

Relate as evidências

Verifique no seu computador

Depois de executar os dois comandos, relate o resultado de cada ferramenta. Indique qual delas fornece evidência de compatibilidade com Destino e qual confirma o registro efetivo da mensagem.

Escreva pelo menos 80 caracteres (0/80).

Passo 8 de 8

Consolidar uma interface estrutural verificável

Aplique o fluxo completo de Protocol em um caso integrado: contrato mínimo, assinaturas compatíveis, acesso somente de leitura e teste comportamental.

Do consumidor ao contrato mínimo

Revise o fluxo de decisão

Comece pelo que o consumidor realmente faz. Se ele encaminha uma mensagem e apenas consulta o nome do destino, o contrato deve expor somente essas duas operações:

  • deliver(...), com a assinatura que o consumidor pode chamar;
  • name, somente para leitura.

Em seguida, anote o ponto de consumo com o protocolo e deixe o mypy verificar cada implementação independente. Membros extras — como uma lista de registros usada no teste — permanecem fora do contrato quando o consumidor não precisa deles.

Fluxo de verificação estrutural

O protocolo fica entre o consumidor e implementações que não precisam compartilhar uma classe base.

Diagrama mostrando um serviço de mensagens usando um contrato mínimo que é atendido separadamente por um destino de console e por um destino que guarda mensagens em memória.

O consumidor enxerga apenas deliver e name; cada implementação pode ter detalhes próprios.

Desafio integrado

Defina e corrija o contrato

Copie o código para um arquivo, por exemplo destinos.py. Complete o protocolo e corrija somente o que for necessário na assinatura de ConsoleDestination.

Repare que MessageService lê destination.name, mas nunca atribui a esse atributo. Portanto, o contrato não deve exigir escrita. A chamada urgent=True também precisa ser aceita por toda implementação compatível.

Código para completar

Substitua os dois ... do protocolo e ajuste o método marcado com # CORRIGIR. Depois execute python -m mypy --strict destinos.py e python destinos.py.

python
from typing import Protocol


class Destination(Protocol):
    # Declare name como uma consulta somente de leitura.
    ...

    # Declare a operação usada pelo consumidor.
    ...


class MessageService:
    def __init__(self, destination: Destination) -> None:
        self.destination = destination

    def notify(self, message: str) -> None:
        print(f"Enviando para {self.destination.name}")
        self.destination.deliver(message, urgent=True)


class ConsoleDestination:
    def __init__(self, name: str) -> None:
        self._name = name

    @property
    def name(self) -> str:
        return self._name

    # CORRIGIR: a assinatura atual não atende ao contrato.
    def deliver(self, message: str, channel: str) -> None:
        print(f"[{channel}] {message}")


class MemoryDestination:
    def __init__(self, name: str) -> None:
        self.name = name
        self.records: list[tuple[str, bool]] = []

    def deliver(self, message: str, *, urgent: bool = False) -> None:
        self.records.append((message, urgent))


memory = MemoryDestination("teste")
service = MessageService(memory)
service.notify("Backup concluído")

assert memory.records == [("Backup concluído", True)]
print("Teste comportamental passou")

Justifique suas decisões

Explique a solução

Relate: (1) como declarou name; (2) qual assinatura final usou em deliver; (3) por que ConsoleDestination e MemoryDestination não precisam herdar de Destination; e (4) o que foram capazes de evidenciar, separadamente, o mypy e o assert.

Escreva pelo menos 180 caracteres (0/180).

Critérios para encerrar

Resumo

Checklist de uma interface estrutural verificável

Use este checklist ao definir um novo Protocol.

  • Observe o consumidor e declare somente os membros que ele usa.
  • Copie para o protocolo as chamadas realmente permitidas: parâmetros, nomes relevantes, valores padrão e retorno.
  • Use atributo gravável apenas se o consumidor também precisar atribuir; para consulta, prefira uma @property sem setter.
  • Anote parâmetros, variáveis ou atributos no ponto de consumo para que o mypy compare as implementações estruturalmente.
  • Não adicione ao protocolo capacidades exclusivas do objeto concreto, como records do substituto de teste.
  • Combine a evidência estática do mypy com testes que confirmem o comportamento em execução.
  • Não enfraqueça o contrato com Any nem imponha herança quando ela não é necessária.

Interface estrutural consolidada

Parabéns! Você concluiu: Definir interfaces estruturais com Protocol

Muito bem! Você consegue derivar um contrato mínimo do consumidor, verificar implementações independentes e separar compatibilidade estática de comportamento testado.

50 XP

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