Python Polimorfismo
A professora entra na sala e diz: "Apresentem-se". Ela não dá uma ordem diferente para cada aluno; dá a mesma, e cada um responde do seu jeito: a Bia diz o nome e a cidade, o Caio fala do time, a Duda conta do cachorro. Em Python, isso tem um nome comprido: polimorfismo (do grego, "muitas formas"). É chamar o mesmo método em objetos diferentes e deixar cada um responder à sua maneira, sem o programa precisar saber de que tipo cada objeto é. Na aula Herança você já viu isso acontecer, quando o cachorro latiu e o gato miou no mesmo for. Agora vamos entender por que isso é tão útil, e descobrir que, em Python, nem é preciso herança para ele funcionar.
- O que é polimorfismo: o mesmo pedido, respostas diferentes.
- A escrever uma função que funciona com qualquer animal, sem nenhum
ifde tipo. - O duck typing: para o Python, o que importa é o objeto ter o método, não a classe dele.
- Que você já usa polimorfismo desde o começo:
len(),+,print(). - A organizar as formas de pagamento da lojinha com polimorfismo.
- A marcar um método que toda filha precisa escrever (
NotImplementedErrore classes abstratas). - Os erros reais: o objeto que não tem o método e os métodos com parâmetros diferentes.
A ideia: o botão "tocar"
Na casa da Ana há um rádio, uma TV e um celular. Os três têm um botão "tocar". Ela aperta o botão do mesmo jeito nos três, e cada aparelho faz a sua coisa: o rádio toca a estação, a TV o filme, o celular a música. A Ana não precisa saber como cada um toca; basta saber que todos sabem tocar.
Em Python, o "botão" é um método com o mesmo nome em várias classes. Quem usa o objeto só chama objeto.tocar(), e cada classe decide o que fazer.
1. O mesmo método, muitas respostas
class Animal:
def __init__(self, nome):
self.nome = nome
def falar(self):
return "..."
class Cachorro(Animal):
def falar(self):
return "Au au!"
class Gato(Animal):
def falar(self):
return "Miau!"
class Vaca(Animal):
def falar(self):
return "Muuu!"
def apresentar(animal):
print(f"{animal.nome} diz: {animal.falar()}")
bichos = [Cachorro("Rex"), Gato("Mimi"), Vaca("Mimosa")]
for bicho in bichos:
apresentar(bicho)
- def falar(self): return "…"
- Cada classe tem o seu
falar, com o mesmo nome e os mesmos parâmetros, e cada um devolve uma coisa diferente. - def apresentar(animal):
- Uma função comum, fora das classes. Ela não sabe se vai receber um cachorro, um gato ou uma vaca. Nem precisa saber.
- animal.falar()
- O ponto de polimorfismo: a mesma linha chama o
falardo cachorro numa volta do laço, o do gato na outra e o da vaca na terceira. Quem escolhe qual método roda é a classe do objeto, na hora da chamada. - bichos = [Cachorro("Rex"), …]
- Uma lista com objetos de classes diferentes. O
fortrata todos do mesmo jeito.
$ python3 muitas_formas.py Rex diz: Au au! Mimi diz: Miau! Mimosa diz: Muuu!
O melhor vem agora: se o petshop começar a atender cavalos, basta escrever a classe Cavalo com o seu falar. A função apresentar e o for não mudam uma letra.
2. Sem polimorfismo: o jeito dos if
Para ver a vantagem, compare com o jeito de quem ainda não usa polimorfismo: uma função só, que pergunta o tipo de cada objeto e decide o que fazer.
class Cachorro:
def __init__(self, nome):
self.nome = nome
class Gato:
def __init__(self, nome):
self.nome = nome
def apresentar(animal):
if isinstance(animal, Cachorro):
print(f"{animal.nome} diz: Au au!")
elif isinstance(animal, Gato):
print(f"{animal.nome} diz: Miau!")
else:
print(f"{animal.nome} diz: ...")
for bicho in [Cachorro("Rex"), Gato("Mimi")]:
apresentar(bicho)
$ python3 com_if.py Rex diz: Au au! Mimi diz: Miau!
Funciona. Mas cada animal novo exige mais um elif nesta função, e em todas as outras funções que fazem coisas parecidas (alimentar, vacinar, dar banho). Com o tempo, o programa vira uma pilha de if isinstance, e esquecer um deles é fácil.
| Forma | Onde fica o "como cada um fala" | Para acrescentar um animal |
|---|---|---|
Polimorfismo: animal.falar() | Dentro de cada classe | Escreve uma classe nova. Nada mais muda |
if isinstance(…) numa função | Espalhado nas funções que usam os animais | Procura e muda todos os if do programa |
Uma regra prática: quando você se pegar escrevendo uma fila de if isinstance(x, A): … elif isinstance(x, B): …, desconfie. Quase sempre aquilo pode virar um método com o mesmo nome em A e em B.
3. Duck typing: se fala como pato…
Até aqui, todos os animais eram filhos de Animal. Mas o Python não exige isso. Quando ele executa coisa.falar(), ele não pergunta "de que classe é a coisa?". Ele só procura um método chamado falar nela. Se achar, chama.
Os programadores chamam isso de duck typing ("tipagem do pato"), por causa de um ditado: "se anda como um pato e grasna como um pato, então é um pato". Não importa o que o objeto é; importa o que ele sabe fazer.
class Pato:
def falar(self):
return "Quack!"
class Robo:
def falar(self):
return "Bip bip!"
class Pessoa:
def falar(self):
return "Olá!"
def fazer_falar(coisa):
print(coisa.falar())
for coisa in [Pato(), Robo(), Pessoa()]:
fazer_falar(coisa)
print(isinstance(Robo(), Pato))
$ python3 pato.py Quack! Bip bip! Olá! False « o robô não é um pato, e mesmo assim funcionou
Três classes sem parentesco nenhum: nenhuma herda da outra, e o robô não é um pato (o isinstance confirma). Mesmo assim, a função fazer_falar funciona com as três, porque as três têm um falar(). É o jeito mais "pythônico" de pensar: escreva funções que pedem uma habilidade ("algo que sabe falar"), e não uma classe ("um Pato").
Missão: o petshop ganhou um robô de brinquedo, e a festa de apresentação quebra quando chega a vez dele. Sem herdar de Animal (o robô não é um animal!), escreva na classe Robo o que falta para ele se apresentar: R2 diz: Bip bip!.
class Animal:
def __init__(self, nome):
self.nome = nome
def falar(self):
return "..."
class Gato(Animal):
def falar(self):
return "Miau!"
class Robo:
def __init__(self, nome):
self.nome = nome
def apresentar(coisa):
print(f"{coisa.nome} diz: {coisa.falar()}")
for coisa in [Gato("Mimi"), Robo("R2")]:
apresentar(coisa)
class Animal:
def __init__(self, nome):
self.nome = nome
def falar(self):
return "..."
class Gato(Animal):
def falar(self):
return "Miau!"
class Robo:
def __init__(self, nome):
self.nome = nome
def falar(self):
return "Bip bip!"
def apresentar(coisa):
print(f"{coisa.nome} diz: {coisa.falar()}")
for coisa in [Gato("Mimi"), Robo("R2")]:
apresentar(coisa)
4. Você já usa polimorfismo
Desde as primeiras aulas, várias coisas do Python funcionam com tipos diferentes, cada tipo do seu jeito:
print(len("Ana"))
print(len(["pão", "leite"]))
print(len({"café": 18.9}))
print(2 + 3)
print("Ana" + " Souza")
print([1, 2] + [3])
$ python3 ja_usa.py 3 « letras do texto 2 « itens da lista 1 « chaves do dicionário 5 « + de números: soma Ana Souza « + de textos: junta [1, 2, 3] « + de listas: emenda
O len() é um "botão" só: o texto conta letras, a lista conta itens, o dicionário conta chaves. O + soma números, junta textos e emenda listas. Por baixo, é exatamente o que você fez com o falar: cada classe (str, list, int) tem um método com o mesmo nome especial, e o Python chama o de cada uma. Na aula Métodos Especiais, você vai ensinar as suas classes a funcionar com len(), + e print().
5. Na lojinha: as formas de pagamento
Um caso de verdade. A lojinha da Ana aceita Pix (preço normal), cartão (5% a mais, a taxa da maquininha) e dinheiro (10% de desconto). Cada forma de pagamento é uma classe com o método total(valor):
class Pix:
nome = "Pix"
def total(self, valor):
return valor
class Cartao:
nome = "Cartão"
def total(self, valor):
return valor * 1.05
class Dinheiro:
nome = "Dinheiro"
def total(self, valor):
return valor * 0.90
compra = 80.00
for forma in [Pix(), Cartao(), Dinheiro()]:
print(f"{forma.nome}: R$ {forma.total(compra):.2f}")
$ python3 pagamento.py Pix: R$ 80.00 Cartão: R$ 84.00 Dinheiro: R$ 72.00
O nome é um atributo de classe (aula Atributos e Métodos): todo Pix se chama "Pix". O caixa da loja só precisa saber que toda forma de pagamento tem nome e total(). Se amanhã a loja aceitar vale-refeição, é uma classe nova, e o caixa não muda.
6. Obrigando as filhas a escrever o método
Às vezes a classe mãe existe só para dizer "todas as filhas precisam ter tal método", mas ela mesma não sabe como fazê-lo. Toda forma geométrica tem área, mas não existe uma fórmula de "área de forma". O costume é a mãe levantar um erro no método, com raise (aula raise):
class Forma:
def area(self):
raise NotImplementedError("cada forma calcula a sua área")
class Quadrado(Forma):
def __init__(self, lado):
self.lado = lado
def area(self):
return self.lado * self.lado
class Circulo(Forma):
def __init__(self, raio):
self.raio = raio
for forma in [Quadrado(3), Circulo(2)]:
print(forma.area())
$ python3 notimpl.py
9
Traceback (most recent call last):
File "/home/ana/notimpl.py", line 20, in <module>
print(forma.area())
~~~~~~~~~~^^
File "/home/ana/notimpl.py", line 3, in area
raise NotImplementedError("cada forma calcula a sua área")
NotImplementedError: cada forma calcula a sua área
O quadrado escreveu o seu area e respondeu 9. O círculo esqueceu, herdou o da mãe e caiu no NotImplementedError ("erro de não implementado"). O rastro mostra o caminho: a chamada na linha 20 levou à linha 3, in area, da classe Forma. A mensagem que a mãe escreveu explica o que faltou. É bem melhor do que um resultado errado e silencioso.
O módulo abc (de abstract base classes, "classes base abstratas") faz uma checagem mais cedo: a filha que esqueceu o método nem consegue criar objetos.
from abc import ABC, abstractmethod
class Forma(ABC):
@abstractmethod
def area(self):
pass
class Circulo(Forma):
def __init__(self, raio):
self.raio = raio
c = Circulo(2)
$ python3 abc_.py
Traceback (most recent call last):
File "/home/ana/abc_.py", line 15, in <module>
c = Circulo(2)
TypeError: Can't instantiate abstract class Circulo without an implementation for abstract method 'area'
"Não dá para criar um objeto da classe abstrata Circulo sem uma implementação do método abstrato area". O @abstractmethod é um decorador (aula Decoradores), que marca o método como obrigatório. Para começar, o NotImplementedError já resolve; o abc aparece em programas maiores.
Quando dá errado
AttributeError
O duck typing tem um preço: o Python só descobre que a "coisa" não sabe falar na hora de chamar.
class Pato:
def falar(self):
return "Quack!"
class Pedra:
pass
def fazer_falar(coisa):
print(coisa.falar())
for coisa in [Pato(), Pedra()]:
fazer_falar(coisa)
$ python3 pato_erro.py
Quack!
Traceback (most recent call last):
File "/home/ana/pato_erro.py", line 15, in <module>
fazer_falar(coisa)
~~~~~~~~~~~^^^^^^^
File "/home/ana/pato_erro.py", line 11, in fazer_falar
print(coisa.falar())
^^^^^^^^^^^
AttributeError: 'Pedra' object has no attribute 'falar'
O pato falou; a pedra, não. O rastro diz: a linha 15 chamou o fazer_falar, e lá dentro, na linha 11, o coisa.falar falhou. "O objeto Pedra não tem o atributo falar" (para o Python, métodos também são atributos). Como consertar: escreva o falar na classe Pedra, ou não ponha pedras na lista das coisas que falam.
Para o polimorfismo funcionar, não basta o mesmo nome: os métodos precisam receber os mesmos dados.
class Pix:
def total(self, valor):
return valor
class Cartao:
def total(self, valor, parcelas):
return valor * 1.05
for forma in [Pix(), Cartao()]:
print(forma.total(80))
$ python3 assinatura.py
80
Traceback (most recent call last):
File "/home/ana/assinatura.py", line 12, in <module>
print(forma.total(80))
~~~~~~~~~~~^^^^
TypeError: Cartao.total() missing 1 required positional argument: 'parcelas'
O Pix funcionou, o cartão quebrou: o total dele pede as parcelas, e o caixa (que trata todo mundo igual) só passou o valor. Como consertar: mantenha os parâmetros iguais em todas as classes. Se o cartão precisa de algo a mais, dê um valor padrão (parcelas=1) ou guarde a informação no __init__: Cartao(parcelas=3).
Você viu acima: o Circulo sem area deu NotImplementedError: cada forma calcula a sua área. Quando essa mensagem aparecer, procure na linha do rastro qual objeto estava sendo usado; a classe dele é a que precisa do método. Como consertar: escreva o método na filha, com o mesmo nome e os mesmos parâmetros da mãe.
Perguntar antes ou pedir desculpas depois?
Se a lista pode ter coisas que não falam, há dois jeitos de evitar o AttributeError:
class Pato:
def falar(self):
return "Quack!"
class Pedra:
pass
for coisa in [Pato(), Pedra()]:
if hasattr(coisa, "falar"):
print(coisa.falar())
else:
print("(não fala)")
for coisa in [Pato(), Pedra()]:
try:
print(coisa.falar())
except AttributeError:
print("(não fala)")
$ python3 eafp.py Quack! (não fala) Quack! (não fala)
| Forma | Quando usar |
|---|---|
Chamar direto: coisa.falar() | O normal. Se a coisa não sabe falar, é um erro de verdade, e o AttributeError avisa |
Perguntar antes: if hasattr(coisa, "falar"): | Quando é esperado que algumas coisas não tenham o método |
Tentar e tratar: try: … except AttributeError: | O mesmo caso, no estilo que muitos programadores Python preferem ("é mais fácil pedir desculpas do que permissão"). Aula try e except |
Testar o tipo: if isinstance(coisa, Pato): | Evite: recusa o robô que também sabe falar |
Referência rápida
| Conceito | Em uma frase | Exemplo |
|---|---|---|
| Polimorfismo | O mesmo método, uma resposta para cada classe | animal.falar() |
| Com herança | As filhas sobrescrevem o método da mãe | class Gato(Animal): def falar(self): … |
| Duck typing | Basta o objeto ter o método; a classe não importa | Robo com falar(), sem herdar de nada |
| Polimorfismo embutido | Funções e operadores que servem para vários tipos | len(), +, print() |
raise NotImplementedError(…) | Na mãe: "toda filha precisa escrever este método" | Forma.area |
ABC + @abstractmethod | A filha sem o método nem cria objetos | class Forma(ABC): |
| Sintoma | Causa provável |
|---|---|
AttributeError: 'Pedra' object has no attribute 'falar' | Um objeto da lista não tem o método |
missing 1 required positional argument só numa das classes | Os métodos de mesmo nome pedem parâmetros diferentes |
NotImplementedError | A filha não escreveu o método que a mãe exige |
Can't instantiate abstract class … without an implementation | O mesmo, com classe abstrata (abc) |
Uma fila de if isinstance | Não é erro, mas é sinal de que um método polimórfico resolveria |
Exercícios
1. A mesma chamada
Complete o laço que pede para cada animal falar (o método devolve o texto):
print(bicho.)
2. O nome do erro
Qual exceção a classe mãe costuma levantar num método que toda filha precisa escrever?
3. O que quer dizer polimorfismo?
4. A classe Robo não herda de Animal, mas tem um método falar(). O que acontece com apresentar(Robo()), se apresentar só chama animal.falar()?
5. A lojinha vai aceitar vale-refeição. Com as formas de pagamento escritas com polimorfismo, o que a Ana precisa mudar?
6. len("Ana") dá 3 e len([1, 2]) dá 2. Isso é um exemplo de quê?
Missão final: pagamento em dinheiro
Missão: o caixa já sabe cobrar no Pix e no cartão. Escreva a classe Dinheiro, com o atributo de classe nome = "Dinheiro" e o método total(self, valor), que dá 10% de desconto (o valor vezes 0.90). Numa compra de 100 reais, o caixa deve mostrar Dinheiro: R$ 90.00, sem você mexer no for.
class Pix:
nome = "Pix"
def total(self, valor):
return valor
class Cartao:
nome = "Cartão"
def total(self, valor):
return valor * 1.05
# escreva aqui a classe Dinheiro
compra = 100.00
for forma in [Pix(), Cartao(), Dinheiro()]:
print(f"{forma.nome}: R$ {forma.total(compra):.2f}")
class Pix:
nome = "Pix"
def total(self, valor):
return valor
class Cartao:
nome = "Cartão"
def total(self, valor):
return valor * 1.05
class Dinheiro:
nome = "Dinheiro"
def total(self, valor):
return valor * 0.90
compra = 100.00
for forma in [Pix(), Cartao(), Dinheiro()]:
print(f"{forma.nome}: R$ {forma.total(compra):.2f}")
Resumo
- Polimorfismo ("muitas formas"): o mesmo método, chamado em objetos de classes diferentes, e cada classe responde do seu jeito.
- Com ele, uma função como
apresentar(animal)funciona com qualquer animal, semifde tipo. Classe nova não muda o código que usa os objetos. - Duck typing: o Python não confere a classe, só se o método existe. Classes sem parentesco funcionam juntas se tiverem os mesmos métodos.
- Para funcionar, os métodos precisam ter o mesmo nome e os mesmos parâmetros.
len(),+eprint()são polimorfismo que você usa desde a primeira aula.- Uma mãe pode exigir um método das filhas com
raise NotImplementedError(ou, em programas maiores, comabc). - Fila de
if isinstanceé sinal de que um método polimórfico resolveria melhor.