Polimorfismo no Python: duck typing, sobrescrita e Protocol

Cansado de programar?

Conheça a melhor e mais completa formação de Python e Django e sinta-se um programador verdadeiramente competente. Além de Python e Django, você também vai aprender Banco de Dados, SQL, HTML, CSS, Javascript, Bootstrap e muito mais!

Quero aprender Python e Django de Verdade! Quero aprender!
Suporte

Tire suas dúvidas diretamente com o professor

Projetos práticos

Projetos práticos voltados para o mercado de trabalho

Prática profissional

Formação moderna com foco na prática profissional

Resposta rápida

Polimorfismo é a capacidade de objetos de classes diferentes responderem à mesma chamada, cada um do seu jeito. Em Python ele aparece por duck typing (basta ter o método), por sobrescrita na herança e por métodos especiais como __len__, __str__ e __add__.

1
2
3
4
5
6
7
8
9
10
class Cachorro:
    def falar(self):
        return "Au au"

class Gato:
    def falar(self):
        return "Miau"

for animal in [Cachorro(), Gato()]:
    print(animal.falar())  # Au au, depois Miau

Resumo em 30 segundos:

  • Mesma interface, comportamentos diferentes: quem chama não precisa saber a classe do objeto.
  • Duck typing: em Python não é preciso herdar de nada, basta o objeto ter o método.
  • Sobrescrita (override) na classe filha é polimorfismo via herança.
  • len(), str() e + são polimórficos porque chamam __len__, __str__ e __add__.
  • Python não tem sobrecarga (overload) como Java: o último def com o mesmo nome vence.

Salve salve Pythonista!

Este é o terceiro post da nossa série sobre Programação Orientada a Objetos. Antes dele vieram a introdução à POO no Python, o post de classes e objetos no Python e o de herança no Python. Se super() e sobrescrita de métodos ainda são novidade para você, vale passar por esse último primeiro.

Aqui você vai ver o que é polimorfismo, como o duck typing faz ele aparecer em todo lugar no Python, como usar sobrescrita, métodos especiais, o que fazer no lugar da sobrecarga de métodos e uma introdução a typing.Protocol. No final, erros comuns com traceback e exercícios resolvidos. Os exemplos foram executados no Python 3.10.

Então… Bora pro post! :rocket:

Vá Direto ao Assunto…

O que é polimorfismo no Python

O que é polimorfismo em Python? Polimorfismo (do grego, “muitas formas”) é a capacidade de objetos de tipos diferentes responderem à mesma operação, cada um com o seu próprio comportamento. O código que usa esses objetos chama sempre a mesma coisa, como animal.falar() ou len(x), e cada objeto decide o que fazer. Isso permite escrever funções genéricas que funcionam com tipos que ainda nem existem.

Você usa polimorfismo desde a primeira aula de Python, talvez sem saber o nome:

1
2
3
4
5
6
print(len("Python"))          # 6: caracteres
print(len([10, 20, 30]))      # 3: itens
print(len({"a": 1, "b": 2}))  # 2: chaves
print(1 + 2)                  # 3: soma
print("Py" + "thon")          # Python: concatenação
print([1, 2] + [3])           # [1, 2, 3]: junta listas

A mesma função len() mede string, lista e dicionário. O mesmo operador + soma números, concatena strings e junta listas. Quem decide o comportamento é o objeto, não a função nem o operador. É exatamente isso que você vai aprender a fazer com as suas próprias classes.

A vantagem prática: o código que usa os objetos fica simples e estável. Em vez de uma cadeia de if tipo == ...: elif tipo == ...:, você chama um método e deixa cada classe cuidar do próprio caso. Para suportar um tipo novo, você cria uma classe nova e não mexe no código que já funciona.

Duck typing: polimorfismo sem herança

Em linguagens como Java, para uma função aceitar objetos diferentes, eles normalmente precisam herdar da mesma classe ou implementar a mesma interface. Python é mais relaxado. A regra é o duck typing: “se anda como um pato e grasna como um pato, então é um pato”. O Python não pergunta de que classe o objeto é, só se ele tem o método que você chamou.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Cachorro:
    def falar(self):
        return "Au au!"


class Gato:
    def falar(self):
        return "Miau!"


class Robo:
    def falar(self):
        return "Bip bop."


def apresentar(coisa):
    print(f"{type(coisa).__name__}: {coisa.falar()}")


for coisa in [Cachorro(), Gato(), Robo()]:
    apresentar(coisa)
1
2
3
Cachorro: Au au!
Gato: Miau!
Robo: Bip bop.

Cachorro, Gato e Robo não têm nenhuma relação de herança entre si. Mesmo assim, apresentar() funciona com os três, porque todos têm falar().

A biblioteca padrão usa isso o tempo todo. Uma função que escreve texto aceita qualquer coisa com um método write(): um arquivo aberto, o terminal (sys.stdout) ou um buffer em memória (io.StringIO):

1
2
3
4
5
6
7
8
9
10
11
12
13
import io
import sys


def salvar_log(destino, mensagem):
    destino.write(f"[LOG] {mensagem}\n")


salvar_log(sys.stdout, "direto no terminal")

memoria = io.StringIO()
salvar_log(memoria, "guardado na memória")
print(repr(memoria.getvalue()))
1
2
[LOG] direto no terminal
'[LOG] guardado na memória\n'

Isso é ótimo para testes: você passa um StringIO no lugar de um arquivo de verdade e confere o que foi escrito.

E se o objeto não tiver o método? O Python só descobre na hora da chamada e lança AttributeError (erro 1 lá embaixo). O estilo recomendado na comunidade é o EAFP (easier to ask for forgiveness than permission): tente usar e trate a exceção, em vez de verificar tudo antes.

1
2
3
4
5
6
7
8
9
10
11
12
class Pedra:
    pass


def apresentar(coisa):
    try:
        print(coisa.falar())
    except AttributeError:
        print(f"{type(coisa).__name__} não sabe falar")


apresentar(Pedra())  # Pedra não sabe falar

Polimorfismo com herança e sobrescrita de métodos

A forma “clássica” de polimorfismo, a que aparece nos livros de POO, usa herança: uma classe mãe define o método e cada filha o sobrescreve com o próprio comportamento. Isso é útil quando as classes também compartilham código, como o método descrever() abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import math


class Forma:
    def area(self):
        raise NotImplementedError("Subclasses devem implementar area()")

    def descrever(self):
        return f"{type(self).__name__} com área {self.area():.2f}"


class Retangulo(Forma):
    def __init__(self, largura, altura):
        self.largura = largura
        self.altura = altura

    def area(self):
        return self.largura * self.altura


class Circulo(Forma):
    def __init__(self, raio):
        self.raio = raio

    def area(self):
        return math.pi * self.raio ** 2


formas = [Retangulo(3, 4), Circulo(1)]
for forma in formas:
    print(forma.descrever())
print(f"Área total: {sum(f.area() for f in formas):.2f}")
1
2
3
Retangulo com área 12.00
Circulo com área 3.14
Área total: 15.14

Repare no detalhe mais interessante: descrever() foi escrito uma vez, na mãe, e chama self.area(). Quando o objeto é um Retangulo, o Python usa o area() do retângulo; quando é um Circulo, usa o do círculo. A mãe nem sabe quais filhas existem. Esse mecanismo, em que a versão executada é decidida pelo objeto em tempo de execução, é o que as provas chamam de polimorfismo dinâmico ou ligação tardia (late binding).

O raise NotImplementedError na mãe é um aviso: “toda filha precisa implementar isso”. Mas o aviso só aparece quando alguém chama o método (erro 2). Se você quer que o Python recuse a classe incompleta já na criação do objeto, o caminho são as classes abstratas, assunto do post sobre abstração e classes abstratas no Python. A sobrescrita e o super() estão explicados em detalhes no post de herança no Python.

Polimorfismo com métodos especiais (__len__, __str__, __add__)

Por que len() funciona com string, lista e dicionário? Porque len(x) na verdade chama x.__len__(). Os operadores e várias funções nativas funcionam assim: são só “atalhos” que chamam um método especial (os dunder methods, de double underscore) do objeto. A lista completa está no modelo de dados da documentação oficial.

Você escreve O Python chama
len(obj) obj.__len__()
str(obj), print(obj), f-string obj.__str__()
repr(obj), o objeto dentro de uma lista obj.__repr__()
a + b a.__add__(b)
a == b a.__eq__(b)
a < b, sorted(), min(), max() a.__lt__(b)

Implemente esses métodos e a sua classe passa a funcionar com as funções nativas, como se fosse um tipo da linguagem:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Playlist:
    def __init__(self, nome, musicas):
        self.nome = nome
        self.musicas = list(musicas)

    def __len__(self):
        return len(self.musicas)

    def __str__(self):
        return f"Playlist {self.nome} ({len(self)} músicas)"

    def __repr__(self):
        return f"Playlist({self.nome!r}, {self.musicas!r})"


rock = Playlist("Rock", ["Paranoid", "Back in Black", "Tempo Perdido"])
print(len(rock))
print(rock)
print(repr(rock))
print(f"Tocando: {rock}")
print(bool(Playlist("Vazia", [])))
1
2
3
4
5
3
Playlist Rock (3 músicas)
Playlist('Rock', ['Paranoid', 'Back in Black', 'Tempo Perdido'])
Tocando: Playlist Rock (3 músicas)
False

A última linha é um bônus: se a classe não define __bool__, o Python usa o __len__ para decidir se o objeto é verdadeiro. Uma playlist vazia é falsa, igual a uma lista vazia. A diferença entre __str__ (texto para o usuário) e __repr__ (texto para o programador) está explicada na seção de métodos especiais do post de classes e objetos.

Com operadores, a ideia é a mesma. O __add__ define o que + faz com a sua classe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Vetor:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, outro):
        if not isinstance(outro, Vetor):
            return NotImplemented
        return Vetor(self.x + outro.x, self.y + outro.y)

    def __eq__(self, outro):
        if not isinstance(outro, Vetor):
            return NotImplemented
        return (self.x, self.y) == (outro.x, outro.y)

    def __repr__(self):
        return f"Vetor({self.x}, {self.y})"


a = Vetor(1, 2)
b = Vetor(3, 4)
print(a + b)                # Vetor(4, 6)
print(a + b == Vetor(4, 6)) # True

O return NotImplemented (sem raise, e não confunda com NotImplementedError) é a forma educada de dizer “não sei somar com esse tipo”. O Python então tenta a operação pelo outro lado e, se ninguém souber, lança um TypeError com uma mensagem clara. O erro 3 mostra o que acontece quando você esquece disso. Esse recurso de dar significado a operadores nas suas classes é chamado de sobrecarga de operadores.

Está curtindo esse conteúdo? :thumbsup:

Que tal receber 30 dias de conteúdo direto na sua Caixa de Entrada?

Sua assinatura não pôde ser validada.
Você fez sua assinatura com sucesso.

Assine as PyDicas e receba 30 dias do melhor conteúdo Python na sua Caixa de Entrada: direto e sem enrolação!

Sobrecarga de métodos existe no Python?

Não do jeito de Java ou C#. Nessas linguagens você pode ter vários métodos somar na mesma classe, com parâmetros diferentes, e o compilador escolhe qual chamar. Em Python, um def com o mesmo nome substitui o anterior, e só a última versão existe (erro 5).

Para os mesmos objetivos, Python tem outras ferramentas:

  • Número variável de argumentos: use valores padrão (def somar(self, a, b=0)) ou *args.
  • Comportamento diferente por tipo do argumento: use functools.singledispatch, que escolhe a implementação pelo tipo do primeiro argumento. Para métodos de classe existe o functools.singledispatchmethod.
1
2
3
4
5
6
7
8
class Calculadora:
    def somar(self, *numeros):
        return sum(numeros)


calc = Calculadora()
print(calc.somar(1, 2))        # 3
print(calc.somar(1, 2, 3, 4))  # 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from functools import singledispatch


@singledispatch
def descrever(valor):
    return f"valor qualquer: {valor!r}"


@descrever.register
def _(valor: int):
    return f"inteiro {valor}, dobro {valor * 2}"


@descrever.register
def _(valor: list):
    return f"lista com {len(valor)} itens"


print(descrever(21))
print(descrever([1, 2, 3]))
print(descrever(3.5))
1
2
3
inteiro 21, dobro 42
lista com 3 itens
valor qualquer: 3.5

A função decorada com @singledispatch é a versão padrão. Cada @descrever.register adiciona uma versão para um tipo, lido da anotação do parâmetro. Como 3.5 é float e não há versão para float, a padrão foi usada. Se decoradores ainda são mistério para você, veja o post sobre decorators em Python.

Protocolos com typing.Protocol

O duck typing é flexível, mas tem um custo: nada no código diz quais métodos um objeto precisa ter para ser aceito, e o erro só aparece na execução. Desde o Python 3.8, o módulo typing oferece o Protocol (PEP 544), que é o duck typing escrito de forma explícita: você descreve os métodos esperados, e qualquer classe que os tenha é aceita, sem herdar do protocolo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from typing import Protocol


class Falante(Protocol):
    def falar(self) -> str:
        ...


class Cachorro:  # não herda de Falante
    def falar(self) -> str:
        return "Au au!"


class Pedra:
    pass


def apresentar(coisa: Falante) -> None:
    print(coisa.falar())


apresentar(Cachorro())  # Au au!
apresentar(Pedra())     # o mypy acusa este erro antes de rodar

Ao executar, o Python não verifica o protocolo: a última linha quebra com o mesmo AttributeError de sempre. Quem usa o protocolo é o verificador de tipos. Rodando o mypy (versão 2.3.1 nos nossos testes) nesse arquivo, o erro aparece antes da execução:

1
2
proto.py:23: error: Argument 1 to "apresentar" has incompatible type "Pedra"; expected "Falante"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

Se você precisa checar em tempo de execução, decore o protocolo com @runtime_checkable e use isinstance():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from typing import Protocol, runtime_checkable


@runtime_checkable
class Falante(Protocol):
    def falar(self) -> str:
        ...


class Cachorro:
    def falar(self) -> str:
        return "Au au!"


class Pedra:
    pass


print(isinstance(Cachorro(), Falante))  # True
print(isinstance(Pedra(), Falante))     # False

Atenção: o isinstance() com protocolo só confere se o método existe, não se os parâmetros e o tipo de retorno batem. Para uma verificação completa, use o mypy (ou o verificador da sua IDE). Protocolos são o assunto mais avançado deste post: se ficou abstrato, tudo bem, volte nele quando começar a usar anotações de tipo nos seus projetos.

Decidir qual forma de polimorfismo usar em cada situação é uma habilidade que se constrói com prática - é isso que os projetos guiados da Jornada Python oferecem, do zero ao Django:

Qual forma de polimorfismo usar

Situação Use Exemplo
Objetos diferentes precisam de um método em comum e não compartilham código Duck typing Pix, Cartao e Boleto com pagar()
As classes compartilham código e só variam em alguns métodos Herança com sobrescrita Forma.descrever() chamando self.area()
As filhas precisam implementar um método e a classe base não pode ser instanciada Classe abstrata (abc.ABC) class Forma(ABC) com @abstractmethod
Documentar e verificar com mypy a interface esperada, sem exigir herança typing.Protocol def apresentar(coisa: Falante)
Sua classe deve funcionar com len(), print(), +, sorted() Métodos especiais __len__, __str__, __add__, __lt__
Uma função deve se comportar diferente conforme o tipo do argumento functools.singledispatch descrever(21) vs descrever([1, 2])

Erros comuns

1. Objeto sem o método esperado (duck typing falhou)

1
2
3
4
5
6
7
8
9
class Pedra:
    pass


def apresentar(coisa):
    print(coisa.falar())


apresentar(Pedra())
1
AttributeError: 'Pedra' object has no attribute 'falar'

Duck typing só funciona se o objeto tiver o método. Correção: implemente falar() na classe, trate o AttributeError com try/except ou declare a interface com Protocol para o mypy avisar antes.

2. Filha que não sobrescreveu o método “obrigatório”

1
2
3
4
5
6
7
8
9
10
11
12
13
class Forma:
    def area(self):
        raise NotImplementedError("Subclasses devem implementar area()")


class Triangulo(Forma):
    def __init__(self, base, altura):
        self.base = base
        self.altura = altura
    # esqueceu de implementar area()


Triangulo(3, 4).area()
1
NotImplementedError: Subclasses devem implementar area()

O objeto foi criado sem problema e o erro só apareceu na chamada. Correção: implemente area() em Triangulo. Para o erro aparecer já ao criar o objeto, transforme Forma em uma classe abstrata com abc.ABC.

3. __add__ que não verifica o tipo do outro operando

1
2
3
4
5
6
7
8
9
10
class Vetor:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, outro):
        return Vetor(self.x + outro.x, self.y + outro.y)


Vetor(1, 2) + 5
1
AttributeError: 'int' object has no attribute 'x'

A mensagem aponta para dentro da sua classe e confunde quem está usando. Correção: comece o __add__ com if not isinstance(outro, Vetor): return NotImplemented. Aí o erro vira o padrão da linguagem, bem mais claro: TypeError: unsupported operand type(s) for +: 'Vetor' and 'int'.

4. __str__ que não devolve string

1
2
3
4
5
6
7
8
9
10
class Produto:
    def __init__(self, nome, preco):
        self.nome = nome
        self.preco = preco

    def __str__(self):
        return self.preco  # devolve float, não str


print(Produto("Caneta", 2.5))
1
TypeError: __str__ returned non-string (type float)

__str__ (e __repr__) precisam devolver str. Da mesma forma, __len__ precisa devolver um inteiro maior ou igual a zero. Correção: return f"{self.nome}: R$ {self.preco:.2f}".

5. Tentar sobrecarga de métodos como em Java

1
2
3
4
5
6
7
8
9
class Calculadora:
    def somar(self, a, b):
        return a + b

    def somar(self, a, b, c):
        return a + b + c


Calculadora().somar(1, 2)
1
TypeError: Calculadora.somar() missing 1 required positional argument: 'c'

O segundo somar apagou o primeiro, e só existe a versão de três parâmetros. Correção: um único método com *args ou valor padrão (def somar(self, a, b, c=0)), ou singledispatch se a diferença for o tipo.

Exercícios resolvidos

Tente resolver cada exercício antes de abrir a solução. Todos os códigos foram executados e as saídas conferidas. Para ir além, pratique com a nossa lista de exercícios de Python.

Exercício 1. Crie as classes Pix, Cartao e Boleto, sem herança entre elas, cada uma com um método pagar(valor) que devolve uma mensagem diferente. Escreva uma função finalizar_compra(metodo, valor) que funcione com as três.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Pix:
    def pagar(self, valor):
        return f"Pix de R$ {valor:.2f} enviado na hora"


class Cartao:
    def pagar(self, valor):
        return f"R$ {valor:.2f} no cartão em 1x"


class Boleto:
    def pagar(self, valor):
        return f"Boleto de R$ {valor:.2f} gerado"


def finalizar_compra(metodo, valor):
    print(metodo.pagar(valor))


for metodo in [Pix(), Cartao(), Boleto()]:
    finalizar_compra(metodo, 99.9)

Saída:

1
2
3
Pix de R$ 99.90 enviado na hora
R$ 99.90 no cartão em 1x
Boleto de R$ 99.90 gerado

Duck typing puro: finalizar_compra() não sabe nem se importa com a classe do objeto. Para aceitar um novo meio de pagamento, basta criar outra classe com pagar().

Exercício 2. (Questão de prova) Qual é a saída do código abaixo?

1
2
3
4
5
6
7
8
9
10
11
12
class Animal:
    def som(self):
        return "..."

class Vaca(Animal):
    def som(self):
        return "Muu"

class Peixe(Animal):
    pass

print([a.som() for a in [Vaca(), Peixe(), Animal()]])

a) ['Muu', 'Muu', '...']
b) ['Muu', '...', '...']
c) ['Muu', None, '...']
d) Um AttributeError, porque Peixe não define som().

Ver solução

Saída: ['Muu', '...', '...']

Resposta: b. Vaca sobrescreve som(), então usa a própria versão. Peixe não sobrescreve, então herda a versão de Animal, que devolve "...". A d está errada porque métodos herdados podem ser chamados normalmente, e a c confunde “não sobrescrever” com “não ter o método”.

Exercício 3. Crie uma classe Carrinho com um método adicionar(item) e implemente __len__ e __str__ para que len(carrinho) devolva a quantidade de itens e print(carrinho) mostre os itens.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Carrinho:
    def __init__(self):
        self.itens = []

    def adicionar(self, item):
        self.itens.append(item)

    def __len__(self):
        return len(self.itens)

    def __str__(self):
        return f"Carrinho com {len(self)} item(ns): {', '.join(self.itens)}"


c = Carrinho()
c.adicionar("livro")
c.adicionar("caneca")
print(len(c))
print(c)

Saída:

1
2
2
Carrinho com 2 item(ns): livro, caneca

Dentro do __str__, len(self) reaproveita o próprio __len__. O print() chama o __str__ automaticamente.

Exercício 4. Crie uma classe Dinheiro(valor, moeda="BRL") que suporte + entre dois objetos Dinheiro da mesma moeda. Somar moedas diferentes deve lançar ValueError.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Dinheiro:
    def __init__(self, valor, moeda="BRL"):
        self.valor = valor
        self.moeda = moeda

    def __add__(self, outro):
        if not isinstance(outro, Dinheiro):
            return NotImplemented
        if outro.moeda != self.moeda:
            raise ValueError("moedas diferentes")
        return Dinheiro(self.valor + outro.valor, self.moeda)

    def __str__(self):
        return f"{self.moeda} {self.valor:.2f}"


print(Dinheiro(10) + Dinheiro(5.5))
try:
    Dinheiro(10) + Dinheiro(2, "USD")
except ValueError as erro:
    print("Erro:", erro)

Saída:

1
2
BRL 15.50
Erro: moedas diferentes

O __add__ devolve um objeto novo em vez de alterar self, como fazem os números. Tipo errado devolve NotImplemented; tipo certo com valor inválido lança ValueError.

Exercício 5. (Questão de prova) Sobre polimorfismo em Python, assinale a alternativa correta:

a) Só existe polimorfismo entre classes que herdam de uma mesma superclasse.
b) Python permite sobrecarga de métodos: dois métodos com o mesmo nome e parâmetros diferentes convivem na mesma classe.
c) Uma função que chama obj.falar() funciona com qualquer objeto que tenha o método falar(), independentemente da classe.
d) O operador + não pode ser redefinido em classes criadas pelo programador.

Ver solução

Resposta: c. Isso é duck typing. A a é falsa porque em Python não é preciso herança para haver polimorfismo. A b é falsa porque o segundo def substitui o primeiro (erro 5). A d é falsa porque __add__ redefine o +, como no Vetor e no Dinheiro acima.

Exercício 6. Faça sorted(), min() e max() funcionarem com uma lista de objetos Produto(nome, preco), ordenando pelo preço, sem passar o parâmetro key.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Produto:
    def __init__(self, nome, preco):
        self.nome = nome
        self.preco = preco

    def __lt__(self, outro):
        return self.preco < outro.preco

    def __repr__(self):
        return f"{self.nome}({self.preco})"


produtos = [Produto("Mouse", 80), Produto("Cabo", 15), Produto("Teclado", 150)]
print(sorted(produtos))
print(min(produtos), max(produtos))

Saída:

1
2
[Cabo(15), Mouse(80), Teclado(150)]
Cabo(15) Teclado(150)

sorted(), min() e max() comparam os itens com <, que chama __lt__. Implementando esse único método, os três passam a funcionar. O __repr__ deixa a lista legível no print().

Exercício 7. Use functools.singledispatch para criar uma função formatar(valor) que devolve float com duas casas, formata cada item de uma list separando por "; " e usa str() para qualquer outro tipo.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from functools import singledispatch


@singledispatch
def formatar(valor):
    return str(valor)


@formatar.register
def _(valor: float):
    return f"{valor:.2f}"


@formatar.register
def _(valor: list):
    return "; ".join(formatar(v) for v in valor)


print(formatar(7))
print(formatar(3.14159))
print(formatar([1, 2.5, "a"]))

Saída:

1
2
3
7
3.14
1; 2.50; a

A versão de list chama formatar() de novo para cada item, e cada um cai na implementação do seu tipo: 1 na padrão, 2.5 na de float.

Exercício 8. Crie um protocolo Desenhavel (com @runtime_checkable) que exige o método desenhar() -> str. Em uma lista com objetos variados, desenhe só os que seguem o protocolo.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from typing import Protocol, runtime_checkable


@runtime_checkable
class Desenhavel(Protocol):
    def desenhar(self) -> str:
        ...


class Quadrado:
    def desenhar(self) -> str:
        return "[]"


class Texto:
    def __init__(self, conteudo: str) -> None:
        self.conteudo = conteudo


itens = [Quadrado(), Texto("oi"), Quadrado()]
print([item.desenhar() for item in itens if isinstance(item, Desenhavel)])

Saída: ['[]', '[]']

Quadrado não herda de Desenhavel, mas tem desenhar(), então isinstance() devolve True. Texto não tem o método e fica de fora.

Conclusão

Polimorfismo é tratar objetos diferentes pela mesma interface e deixar cada um decidir o que fazer. Em Python ele aparece em três formas principais: duck typing (basta ter o método), herança com sobrescrita (a filha redefine o método da mãe) e métodos especiais (sua classe passa a funcionar com len(), print(), + e sorted()). No lugar da sobrecarga de métodos, use argumentos padrão, *args ou singledispatch. E quando quiser documentar e verificar a interface esperada, typing.Protocol é o caminho.

A série de POO fecha com abstração e classes abstratas no Python, onde você vai ver como obrigar as filhas a implementar certos métodos. E se quiser controlar o acesso aos atributos das suas classes, veja como utilizar @property no Python. Para aprender POO com projetos guiados, conheça o nosso curso de Python completo.

Nos vemos no próximo post!

Perguntas frequentes

O que é polimorfismo em Python?

Polimorfismo é a capacidade de objetos de classes diferentes responderem à mesma chamada, cada um do seu jeito. Se Cachorro e Gato têm um método falar(), um loop pode chamar animal.falar() sem saber a classe de cada objeto. Em Python ele aparece por duck typing, por sobrescrita de métodos e por métodos especiais como __len__ e __add__.

O que é duck typing?

Duck typing é o estilo em que o Python se importa com o que o objeto sabe fazer, e não com a classe dele: ‘se anda como pato e grasna como pato, é um pato’. Uma função que chama obj.write(texto) funciona com qualquer objeto que tenha write(), seja um arquivo, sys.stdout ou um io.StringIO, sem precisar de herança.

Qual a diferença entre polimorfismo e herança?

Herança é um mecanismo de reaproveitamento: a classe filha recebe atributos e métodos da mãe. Polimorfismo é o efeito de tratar objetos diferentes pela mesma interface. A herança com sobrescrita é uma forma de obter polimorfismo, mas em Python não é a única: com duck typing, classes sem nenhuma relação de herança também são polimórficas.

Python tem sobrecarga de métodos?

Não como em Java ou C#. Se você definir dois métodos com o mesmo nome na mesma classe, o último substitui o primeiro. Para aceitar números diferentes de argumentos, use valores padrão ou *args. Para escolher a implementação pelo tipo do argumento, use functools.singledispatch (funções) ou functools.singledispatchmethod (métodos).

Qual a diferença entre sobrescrita e sobrecarga?

Sobrescrita (override) é a classe filha redefinir um método herdado com o mesmo nome, e o Python suporta normalmente. Sobrecarga (overload) é ter vários métodos com o mesmo nome e parâmetros diferentes na mesma classe, e o Python não suporta: o último def vence. Em provas, sobrescrita costuma ser chamada de polimorfismo dinâmico.

O que são métodos especiais (dunder) em Python?

São métodos com dois underlines no início e no fim, como __len__, __str__ e __add__, que o Python chama por você quando você usa funções nativas e operadores. len(obj) chama obj.__len__(), str(obj) e print(obj) chamam __str__ e a + b chama a.__add__(b). Implementá-los faz suas classes funcionarem como os tipos nativos.

O que é typing.Protocol?

typing.Protocol (Python 3.8+) define uma interface estrutural: uma lista de métodos que um objeto precisa ter para ser aceito, sem exigir herança. Verificadores como o mypy conferem se o objeto tem esses métodos. Em tempo de execução o Python não impede nada, a menos que você use @runtime_checkable e teste com isinstance().

Em Python, o polimorfismo exige que as classes tenham uma superclasse em comum?

Não. Essa é uma pegadinha comum em provas baseadas em Java. Em Python basta que os objetos tenham o método chamado (duck typing): Cachorro, Gato e Robo podem ser classes independentes e ainda assim um loop que chama obj.falar() funciona com todos. A superclasse comum é opcional.

Começe agora sua Jornada na Programação!

Não deixe para amanhã o sucesso que você pode começar a construir hoje!

#newsletter Olá :wave: Curtiu o artigo? Então faça parte da nossa Newsletter! Privacidade Não se preocupe, respeitamos sua privacidade. Você pode se descadastrar a qualquer momento.