Trilha de aprendizado · Nível 7 · Tutorial 6

Aplicar polimorfismo pelo comportamento dos objetos

Escrever código que trabalha com diferentes objetos por meio de operações compatíveis, sem depender de verificações de tipo para cada implementação.

  • Nível: Intermediário
  • Duração: 18 min
  • 7 passos
Aplicar polimorfismo pelo comportamento dos objetos

O que você vai percorrer

  1. Uma chamada, comportamentos diferentes Entenda como a mesma chamada de método pode executar implementações diferentes conforme o objeto que recebe a chamada. 2 min
  2. Compatibilidade sem herança obrigatória Criar uma classe independente que participa da mesma chamada dos formatadores existentes por oferecer o comportamento esperado. 2 min
  3. Definir o que a operação promete Descreva as expectativas que tornam diferentes formatadores compatíveis com a mesma chamada. 3 min
  4. Escrever um consumidor sem decisões por tipo Crie uma função consumidora que chama a operação compatível dos formatadores sem escolher uma implementação por classe. 3 min
  5. Trocar o colaborador sem mudar quem o utiliza Configure o mesmo PainelDeAvisos com formatadores diferentes, preservando o código que delega a formatação e registra o histórico. 2 min
  6. Reconhecer métodos que não são substituíveis Diagnostique por que objetos com um método de mesmo nome ainda podem ser incompatíveis com a chamada e com o comportamento esperado pelo consumidor. 3 min
  7. Aplicar e revisar a substituição por comportamento Implemente um formatador independente, use-o em dois consumidores existentes e verifique sua compatibilidade sem alterar o código desses consumidores. 4 min

O que você vai aprender

  • Implementar classes diferentes que atendem à mesma operação esperada pelo código consumidor.
  • Criar uma função que utiliza esses objetos sem selecionar comportamentos por tipo.
  • Substituir um colaborador por outro sem alterar o código que o utiliza.
  • Identificar incompatibilidades de argumentos, resultados ou significado entre métodos aparentemente semelhantes.

Antes de começar

  • Compor objetos com responsabilidades distintas
  • Especializar classes com herança e super
  • Definir funções com parâmetros e retorno

Passo 1 de 7

Uma chamada, comportamentos diferentes

Entenda como a mesma chamada de método pode executar implementações diferentes conforme o objeto que recebe a chamada.

A mesma operação, resultados diferentes

Uso uniforme de objetos

Polimorfismo permite usar da mesma maneira objetos que oferecem comportamentos compatíveis. No exemplo, os dois objetos respondem à chamada formatar(texto), mas cada um apresenta o texto de um jeito.

A subclasse aproveita a sobrescrita que você já conhece: ela redefine formatar sem mudar a forma de chamar o método.

Dois formatadores

Execute este código e compare os resultados.

python
class FormatadorSimples:
    def formatar(self, texto):
        return texto


class FormatadorEntreColchetes(FormatadorSimples):
    def formatar(self, texto):
        return f"[{texto}]"


simples = FormatadorSimples()
com_colchetes = FormatadorEntreColchetes()

print(simples.formatar("Alerta"))
print(com_colchetes.formatar("Alerta"))

# Saída:
# Alerta
# [Alerta]

O objeto determina a implementação

A escolha ocorre durante a execução

Nas duas chamadas, o nome do método e o argumento são iguais. Python procura a implementação adequada conforme o objeto que está antes do ponto.

Se o objeto é FormatadorSimples, executa a implementação da classe base. Se é FormatadorEntreColchetes, executa a implementação sobrescrita. O nome da variável não determina essa escolha.

Uma chamada encaminhada a duas implementações

Diagrama dividido em dois caminhos: a mesma chamada formatar com o texto Alerta chega ao formatador simples e produz Alerta; no outro caminho, chega ao formatador entre colchetes e produz [Alerta].

A chamada tem a mesma forma; o objeto receptor define qual implementação será executada.

Dica

Olhe para o receptor

Para prever qual método será executado, identifique o objeto que recebe a chamada — a expressão à esquerda do ponto.

Preveja a execução

Qual será o resultado?

Considere as classes anteriores:

formatador = FormatadorEntreColchetes()
resultado = formatador.formatar("Aviso")

Qual valor será armazenado em resultado?

Passo 2 de 7

Compatibilidade sem herança obrigatória

Criar uma classe independente que participa da mesma chamada dos formatadores existentes por oferecer o comportamento esperado.

Mais de um caminho para a mesma operação

O comportamento pode ser compatível

No step anterior, objetos diferentes responderam à chamada formatar(texto). Eles podem participar desse uso polimórfico mesmo sem pertencer à mesma hierarquia.

Em Python, o código pode usar as operações que um objeto oferece, em vez de exigir uma classe concreta. Essa abordagem é conhecida como duck typing: se o objeto oferece o comportamento necessário para aquele uso, ele pode ser utilizado.

Herança e compatibilidade de comportamento

À esquerda, uma subclasse recebe a operação de uma classe base. À direita, objetos sem relação de herança chegam ao mesmo ponto de uso porque todos oferecem uma operação compatível.

Comparação entre uma hierarquia de herança e objetos independentes conectados à mesma operação.

A herança é uma forma de compartilhar uma operação, mas não é requisito para usar objetos de modo polimórfico.

Um formatador independente

Três objetos, a mesma chamada

FormatadorComPrefixo não herda de FormatadorSimples. Ainda assim, sua instância recebe a mesma chamada porque também define formatar(texto).

python
class FormatadorSimples:
    def formatar(self, texto):
        return texto


class FormatadorEntreColchetes(FormatadorSimples):
    def formatar(self, texto):
        return f"[{texto}]"


class FormatadorComPrefixo:
    def __init__(self, prefixo):
        self.prefixo = prefixo

    def formatar(self, texto):
        return f"{self.prefixo}{texto}"


simples = FormatadorSimples()
entre_colchetes = FormatadorEntreColchetes()
com_prefixo = FormatadorComPrefixo("AVISO: ")

print(simples.formatar("Manutenção"))
print(entre_colchetes.formatar("Manutenção"))
print(com_prefixo.formatar("Manutenção"))

# Saída:
# Manutenção
# [Manutenção]
# AVISO: Manutenção

Dica

Observe o ponto de uso

As três últimas chamadas têm a mesma forma: objeto.formatar("Manutenção"). Quem chama não precisa saber se o método foi herdado ou definido diretamente pela classe do objeto.

Complete a classe independente

Implemente a operação

Considere o método abaixo:

def formatar(self, texto):

Complete a instrução para que FormatadorComPrefixo("# ").formatar("Alerta") devolva # Alerta:

return ____

Passo 3 de 7

Definir o que a operação promete

Descreva as expectativas que tornam diferentes formatadores compatíveis com a mesma chamada.

As quatro partes do contrato

Mais do que ter o mesmo método

Para decidir se um objeto pode ser usado como formatador, precisamos explicitar o que o código consumidor espera de formatar. Esse contrato informal descreve quatro dimensões:

  • Argumentos: o que a chamada fornece.
  • Resultado: o que o método devolve.
  • Significado: o que esse resultado representa.
  • Efeitos: o que pode ou não acontecer além do retorno.

O contrato é informal porque está registrado em linguagem natural, não porque seja opcional.

Diagrama de uma string entrando em um formatador e saindo decorada, com símbolos indicando ausência de impressão e de alteração de estado.

A chamada fornece uma string e recebe outra string que ainda contém a mensagem original. Imprimir ou alterar o estado do formatador não faz parte da operação.

Registrar a promessa

Exemplo

Contrato informal de formatar

formatar(texto) recebe uma string como argumento posicional e devolve uma string. O resultado preserva o texto original sem modificá-lo, embora possa acrescentar prefixos, sufixos ou outros elementos decorativos. A operação apenas produz e retorna o resultado: não imprime mensagens nem altera o estado do formatador.

O mesmo contrato em uma docstring

A docstring registra a expectativa perto da operação, sem exigir ferramentas de tipagem.

python
class FormatadorSimples:
    def formatar(self, texto):
        """Formata uma string e devolve outra string.

        O resultado preserva o texto original e pode acrescentar
        decoração. O método não imprime mensagens nem altera o
        estado do formatador.
        """
        return texto

Dica

Compatibilidade depende do uso

As assinaturas não precisam ser textualmente idênticas. Um método pode ter um parâmetro opcional adicional e ainda aceitar a chamada formatador.formatar(texto). O essencial é atender à chamada e às expectativas de resultado, significado e efeitos.

Ler o contrato pelas expectativas

Associe cada expectativa

Relacione cada dimensão do contrato de formatar à expectativa correspondente.

Toque em um item e depois no par correspondente.

Passo 4 de 7

Escrever um consumidor sem decisões por tipo

Crie uma função consumidora que chama a operação compatível dos formatadores sem escolher uma implementação por classe.

Quando o consumidor escolhe a classe

O papel do consumidor

Código consumidor é aquele que utiliza a operação pública oferecida por outro objeto. Neste caso, ele recebe um formatador e precisa usar formatar(texto).

Se o consumidor verifica a classe para decidir como montar cada resultado, ele assume responsabilidades que pertencem aos formatadores. Além disso, cada nova implementação exige alterar essa cadeia de decisões.

Consumidor dependente das classes

Este código conhece cada implementação e reproduz suas diferenças:

python
def formatar_mensagens(formatador, textos):
    resultados = []

    for texto in textos:
        if isinstance(formatador, FormatadorEntreColchetes):
            resultados.append(f"[{texto}]")
        elif isinstance(formatador, FormatadorComPrefixo):
            resultados.append(f"{formatador.prefixo}{texto}")
        else:
            resultados.append(texto)

    return resultados

Uma chamada uniforme

Delegue a diferença ao objeto

O consumidor não precisa saber como cada classe formata. Ele apenas faz a chamada prevista pelo contrato. Durante a execução, o próprio objeto recebido determina qual implementação de formatar será usada.

Ramificações versus chamada uniforme

À esquerda, o fluxo se divide conforme a classe. À direita, todas as mensagens seguem pela mesma chamada, enquanto cada objeto produz seu próprio resultado.

Comparação entre um fluxo com várias ramificações por classe e outro com uma única chamada encaminhada a formatadores diferentes.

No fluxo polimórfico, o consumidor conhece a operação esperada, não a lista de classes que podem realizá-la.

Função polimórfica

A função percorre os textos, chama a mesma operação em cada item e devolve a lista de resultados:

python
def formatar_mensagens(formatador, textos):
    resultados = []

    for texto in textos:
        resultado = formatador.formatar(texto)
        resultados.append(resultado)

    return resultados


mensagens = ["Sistema iniciado", "Atualização disponível"]

print(formatar_mensagens(FormatadorSimples(), mensagens))
print(formatar_mensagens(FormatadorEntreColchetes(), mensagens))
print(formatar_mensagens(FormatadorComPrefixo("AVISO: "), mensagens))

O que deve desaparecer

Dica

Polimorfismo não proíbe toda validação

Uma verificação pode continuar sendo adequada para validar uma entrada. Por exemplo, a função poderia rejeitar um valor que não atende à regra definida para textos. O problema é usar type ou isinstance para selecionar como cada classe deve formatar.

Exemplo

Duas intenções diferentes

Validar: “textos atende às regras de entrada da função?”

Selecionar por tipo: “Se o formatador pertence à classe A, faça isto; se pertence à classe B, faça aquilo.”

É essa segunda decisão que a chamada uniforme elimina. Um objeto que atende ao contrato de formatar(texto) pode participar sem ganhar um ramo próprio.

Refatore o consumidor

Código para refatorar

Reescreva esta função sem isinstance, preservando o processamento de todos os textos:

python
def preparar_avisos(formatador, textos):
    if isinstance(formatador, FormatadorSimples):
        return [formatador.formatar(texto) for texto in textos]
    elif isinstance(formatador, FormatadorEntreColchetes):
        return [formatador.formatar(texto) for texto in textos]
    elif isinstance(formatador, FormatadorComPrefixo):
        return [formatador.formatar(texto) for texto in textos]

    return []

Chame o comportamento diretamente

Refatore a função e explique, em uma ou duas frases, por que uma nova classe compatível não precisa ser adicionada a uma cadeia de elif.

Escreva pelo menos 80 caracteres (0/80).

Passo 5 de 7

Trocar o colaborador sem mudar quem o utiliza

Configure o mesmo PainelDeAvisos com formatadores diferentes, preservando o código que delega a formatação e registra o histórico.

O painel delega a formatação

Composição com um colaborador substituível

O PainelDeAvisos mantém seu próprio histórico, mas entrega a formatação a um objeto recebido no __init__. Como os formatadores oferecem a operação compatível formatar(texto), o painel não precisa saber qual classe recebeu: ele apenas chama o método, obtém o resultado e o registra.

Duas configurações do mesmo consumidor

Diagrama com dois painéis de avisos idênticos ligados a formatadores diferentes. Um produz uma mensagem entre colchetes; o outro acrescenta um prefixo.

O código do PainelDeAvisos permanece igual. Apenas o colaborador fornecido na criação muda: um resultado pode ser [Manutenção às 18h]; o outro, URGENTE: Manutenção às 18h.

Manter as responsabilidades separadas

Formatadores e PainelDeAvisos

O painel consome apenas formatar(texto). Os detalhes de apresentação ficam nos formatadores.

python
class FormatadorSimples:
    def formatar(self, texto):
        return texto


class FormatadorEntreColchetes(FormatadorSimples):
    def formatar(self, texto):
        return f"[{texto}]"


class FormatadorComPrefixo:
    def __init__(self, prefixo):
        self.prefixo = prefixo

    def formatar(self, texto):
        return f"{self.prefixo}: {texto}"


class PainelDeAvisos:
    def __init__(self, formatador):
        self.formatador = formatador
        self.historico = []

    def registrar_aviso(self, mensagem):
        mensagem_formatada = self.formatador.formatar(mensagem)
        self.historico.append(mensagem_formatada)
        return mensagem_formatada

Dica

Quem faz o quê?

O formatador decide a apresentação da mensagem. O painel decide quando registrar o aviso e mantém seu próprio histórico. Trocar o colaborador não deve transferir essas responsabilidades nem exigir uma decisão por tipo dentro de registrar_aviso.

Mudar a configuração, não o consumidor

Dois painéis, duas configurações

A escolha do comportamento ocorre na montagem dos objetos. O método registrar_aviso não muda.

python
painel_interno = PainelDeAvisos(
    FormatadorEntreColchetes()
)

painel_urgente = PainelDeAvisos(
    FormatadorComPrefixo("URGENTE")
)

print(painel_interno.registrar_aviso("Manutenção às 18h"))
print(painel_urgente.registrar_aviso("Manutenção às 18h"))

print(painel_interno.historico)
print(painel_urgente.historico)

Exemplo

Resultado esperado

[Manutenção às 18h]
URGENTE: Manutenção às 18h
['[Manutenção às 18h]']
['URGENTE: Manutenção às 18h']

Cada painel possui seu próprio histórico. A diferença de apresentação vem do colaborador escolhido na criação.

Configuração ou mudança interna?

Verifique a substituição

Para usar um prefixo no lugar de colchetes, é necessário adicionar um if por tipo dentro de PainelDeAvisos.registrar_aviso.

Passo 6 de 7

Reconhecer métodos que não são substituíveis

Diagnostique por que objetos com um método de mesmo nome ainda podem ser incompatíveis com a chamada e com o comportamento esperado pelo consumidor.

Mesmo nome, contratos diferentes

O nome do método é apenas o começo

Para substituir um formatador, não basta que o objeto tenha um método chamado formatar. O método precisa aceitar a chamada feita pelo consumidor, devolver o resultado esperado, preservar o significado da operação e evitar efeitos não previstos.

Nosso consumidor chama formatar(texto) e espera receber uma string que preserve o texto original, sem impressão nem alteração de estado no formatador.

Quatro formas de incompatibilidade

Um objeto pode falhar ao receber a chamada, produzir um resultado inadequado, realizar outra operação ou provocar um efeito inesperado.

Diagrama em que a mesma mensagem segue para diferentes objetos: um exige uma entrada adicional, outro gera uma saída vazia, outro transforma a mensagem em uma medida e outro altera um contador interno.

A presença de formatar não garante compatibilidade de argumentos, resultado, significado ou efeitos.

Examine as implementações

Onde cada objeto rompe o contrato?

Compare as classes abaixo. EstiloOpcional continua aceitando formatar(texto) porque seu argumento adicional tem valor padrão. As demais classes possuem o nome esperado, mas violam pelo menos uma parte do contrato.

Formatadores aparentemente semelhantes

Observe argumentos, retorno, significado e alterações de estado.

python
class ExigeEstilo:
    def formatar(self, texto, estilo):
        return f"{estilo}{texto}{estilo}"


class EstiloOpcional:
    def formatar(self, texto, estilo="*"):
        return f"{estilo}{texto}{estilo}"


class ApenasImprime:
    def formatar(self, texto):
        print(texto)
        # Sem return explícito: o resultado é None.


class MedeTexto:
    def formatar(self, texto):
        return f"{len(texto)} caracteres"


class ContaChamadas:
    def __init__(self):
        self.chamadas = 0

    def formatar(self, texto):
        self.chamadas += 1
        return f"[{texto}]"

Classifique as incompatibilidades

Associe cada classe ao problema principal

Relacione cada implementação à forma como ela deixa de atender ao contrato.

Toque em um item e depois no par correspondente.

Exceção não é o único sinal

Atenção

Código executado não significa contrato atendido

A falta de um argumento pode produzir um TypeError imediatamente. Já MedeTexto e ContaChamadas aceitam a chamada e podem terminar sem exceção, embora violem o significado ou os efeitos previstos. Não capture erros indiscriminadamente para esconder a incompatibilidade: corrija a implementação ou escolha um colaborador compatível.

Resumo

Diagnóstico em quatro perguntas

  • O método aceita a chamada que o consumidor realmente faz?
  • O resultado possui a forma esperada, como uma string em vez de None?
  • O resultado mantém o significado prometido, incluindo a preservação da mensagem?
  • A operação evita impressões e alterações de estado que não fazem parte do contrato?

Passo 7 de 7

Aplicar e revisar a substituição por comportamento

Implemente um formatador independente, use-o em dois consumidores existentes e verifique sua compatibilidade sem alterar o código desses consumidores.

A substituição que vamos realizar

Um novo comportamento, os mesmos consumidores

Nesta prática, você criará FormatadorComMarcadores, uma classe independente que envolve cada texto com marcadores configuráveis.

Ela será usada por formatar_mensagens e por PainelDeAvisos. Esses consumidores continuarão chamando apenas formatar(texto): nenhuma decisão baseada na classe será adicionada a eles.

Compatibilidade em dois consumidores

O mesmo objeto pode colaborar com consumidores diferentes quando oferece a operação esperada por ambos.

Diagrama com um formatador de marcadores conectado a uma função que processa listas e a um painel que registra avisos.

A substituição acontece na configuração e nas chamadas externas; o código interno dos consumidores permanece igual.

Implemente e execute o script

Sua tarefa

Crie um arquivo .py no seu computador e copie o código abaixo. Complete somente os dois trechos indicados na classe FormatadorComMarcadores.

O método formatar deve aceitar uma string por posição, devolver outra string com os marcadores e preservar integralmente o texto recebido. Ele não deve imprimir nada nem modificar a configuração do objeto.

Código-base completo

Execute com Python 3 depois de completar a nova classe. Não altere formatar_mensagens nem PainelDeAvisos.

python
class FormatadorSimples:
    def formatar(self, texto):
        return texto


class FormatadorEntreColchetes(FormatadorSimples):
    def formatar(self, texto):
        return f"[{texto}]"


class FormatadorComPrefixo:
    def __init__(self, prefixo):
        self.prefixo = prefixo

    def formatar(self, texto):
        return f"{self.prefixo}{texto}"


class FormatadorComMarcadores:
    def __init__(self, inicio, fim):
        # Substitua o pass: guarde os dois marcadores no objeto.
        pass

    def formatar(self, texto):
        # Substitua o pass: devolva inicio + texto + fim.
        pass


def formatar_mensagens(formatador, textos):
    return [formatador.formatar(texto) for texto in textos]


class PainelDeAvisos:
    def __init__(self, formatador):
        self.formatador = formatador
        self.historico = []

    def registrar(self, mensagem):
        mensagem_formatada = self.formatador.formatar(mensagem)
        self.historico.append(mensagem_formatada)
        return mensagem_formatada


# Configuração com o novo colaborador
formatador = FormatadorComMarcadores("<", ">")
configuracao_antes = (formatador.inicio, formatador.fim)

# Consumidor 1: função
resultados = formatar_mensagens(
    formatador,
    ["Atenção", "Reunião às 14h"],
)

# Consumidor 2: objeto composto
painel = PainelDeAvisos(formatador)
registrado = painel.registrar("Servidor reiniciado")

print(resultados)
print(registrado)
print(painel.historico)
print(configuracao_antes == (formatador.inicio, formatador.fim))

Exemplo

Resultado esperado

A execução deve apresentar exatamente estas quatro linhas, sem mensagens extras:

['<Atenção>', '<Reunião às 14h>']
<Servidor reiniciado>
['<Servidor reiniciado>']
True

As três primeiras linhas confirmam o retorno textual e a preservação das mensagens. A última confirma que os marcadores não foram modificados durante a formatação. Se surgirem linhas extras, verifique se você colocou algum print dentro do formatador.

Revise sua implementação

Compare com o contrato

Antes de responder, confira quatro pontos: formatar(texto) aceita a chamada usada pelos consumidores; o resultado é uma string; o conteúdo original permanece no resultado; e a operação não imprime nem altera os marcadores.

Observe também que FormatadorComMarcadores não precisa herdar de outro formatador. A compatibilidade vem do comportamento oferecido, não de uma classe base comum.

Registre a solução e o diagnóstico

Apresente sua implementação de FormatadorComMarcadores. Depois, relate o que apareceu ao usar o objeto em formatar_mensagens e em PainelDeAvisos. Explique por que nenhum dos dois consumidores precisou ser modificado.

Escreva pelo menos 180 caracteres (0/180).

Síntese do tutorial

Resumo

Critérios para substituição por comportamento

Um objeto pode substituir outro quando atende às chamadas e às expectativas relevantes do consumidor.

  • O consumidor chama uma operação pública compatível, sem selecionar implementações com type ou isinstance.
  • A compatibilidade envolve argumentos aceitos, tipo de resultado, significado da operação e efeitos observáveis.
  • Classes independentes podem participar do mesmo uso polimórfico; herança é uma relação de especialização, não um requisito para toda substituição.
  • Novos colaboradores são integrados pela criação e configuração dos objetos, preservando o código que os utiliza.
  • Ter um método com o mesmo nome não basta se a chamada, o retorno, o significado ou os efeitos forem incompatíveis.

Tutorial concluído

Parabéns! Você concluiu: Aplicar polimorfismo pelo comportamento dos objetos

Prática concluída! Agora você pode criar consumidores que variam o comportamento por meio dos objetos recebidos, sem ramificações para cada classe concreta.

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