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.

Você vai aprender
  • O que é polimorfismo: o mesmo pedido, respostas diferentes.
  • A escrever uma função que funciona com qualquer animal, sem nenhum if de 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 (NotImplementedError e 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 falar do 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 for trata 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.

FormaOnde fica o "como cada um fala"Para acrescentar um animal
Polimorfismo: animal.falar()Dentro de cada classeEscreve uma classe nova. Nada mais muda
if isinstance(…) numa funçãoEspalhado nas funções que usam os animaisProcura 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.

Para ir além: classes abstratas

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

O objeto não tem o método: 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.

Os métodos têm parâmetros diferentes

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).

A filha esqueceu de escrever o método obrigatório

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)
FormaQuando 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

ConceitoEm uma fraseExemplo
PolimorfismoO mesmo método, uma resposta para cada classeanimal.falar()
Com herançaAs filhas sobrescrevem o método da mãeclass Gato(Animal): def falar(self): …
Duck typingBasta o objeto ter o método; a classe não importaRobo com falar(), sem herdar de nada
Polimorfismo embutidoFunções e operadores que servem para vários tiposlen(), +, print()
raise NotImplementedError(…)Na mãe: "toda filha precisa escrever este método"Forma.area
ABC + @abstractmethodA filha sem o método nem cria objetosclass Forma(ABC):
SintomaCausa 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 classesOs métodos de mesmo nome pedem parâmetros diferentes
NotImplementedErrorA filha não escreveu o método que a mãe exige
Can't instantiate abstract class … without an implementationO mesmo, com classe abstrata (abc)
Uma fila de if isinstanceNã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):

for bicho in bichos:
    print(bicho.)

2. O nome do erro

Qual exceção a classe mãe costuma levantar num método que toda filha precisa escrever?

raise ("cada forma calcula a sua área")

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, sem if de 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(), + e print() 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, com abc).
  • Fila de if isinstance é sinal de que um método polimórfico resolveria melhor.