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
defcom 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! ![]()
Vá Direto ao Assunto…
- O que é polimorfismo no Python
- Duck typing: polimorfismo sem herança
- Polimorfismo com herança e sobrescrita de métodos
- Polimorfismo com métodos especiais (
__len__,__str__,__add__) - Sobrecarga de métodos existe no Python?
- Protocolos com typing.Protocol
- Qual forma de polimorfismo usar
- Erros comuns
- Exercícios resolvidos
- Conclusão
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()oulen(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? ![]()
Que tal receber 30 dias de conteúdo direto na sua Caixa de Entrada?
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 ofunctools.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.
"Porque o Senhor dá a sabedoria, e da sua boca vem a inteligência e o entendimento" Pv 2:6