Polimorfismo e Métodos Mágicos

As últimas duas lições cobriram a "forma" das classes e herança. Esta lição cobre o "espírito." Polimorfismo permite que objetos de diferentes classes sejam chamados da mesma forma — você não precisa saber sua classe específica, apenas que eles têm o método necessário. Métodos mágicos fazem classes personalizadas se comportarem tão naturalmente quanto tipos embutidos.


1. O que é Polimorfismo

Polimorfismo significa "muitas formas" — a mesma interface, implementações diferentes:

PYTHON
class Cachorro:
    def falar(self):
        return "Au au!"

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

class Pato:
    def falar(self):
        return "Quack!"

# Polimorfismo: independente da classe, desde que tenha um método falar()
animais = [Cachorro(), Gato(), Pato()]
for animal in animais:
    print(animal.falar())

Saída:

TEXT
Au au!
Miau!
Quack!
💡 Duck Typing: "Se anda como um pato e faz quack como um pato, é um pato." Python não se importa com o tipo de um objeto — apenas se ele tem o método que você precisa. Este é um dos designs mais flexíveis do Python.

Exemplo: Cálculo de Área de Formas (Dificuldade ⭐⭐)

PYTHON
class Retangulo:
    def __init__(self, largura, altura):
        self.largura = largura
        self.altura = altura
    
    def area(self):
        return self.largura * self.altura

class Circulo:
    def __init__(self, raio):
        self.raio = raio
    
    def area(self):
        return 3.14159 * self.raio ** 2

class Triangulo:
    def __init__(self, base, altura):
        self.base = base
        self.altura = altura
    
    def area(self):
        return 0.5 * self.base * self.altura

# Polimorfismo — independente da forma, apenas calcule a área
formas = [Retangulo(3, 4), Circulo(5), Triangulo(6, 8)]
for forma in formas:
    print(f"Área: {forma.area():.2f}")
▶ Experimente

Saída:

TEXT
Área: 12.00
Área: 78.54
Área: 24.00

2. Métodos Mágicos: Tornando Classes Mais "Pytônicas"

Métodos mágicos são métodos especiais com prefixos e sufixos de sublinhado duplo — __init__, __str__, __len__, etc. Eles fazem classes personalizadas se comportarem como tipos embutidos.

__str__ e __repr__: Impressão Mais Amigável

PYTHON
class Pessoa:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade
    
    def __str__(self):
        """Representação amigável para o usuário"""
        return f"{self.nome} ({self.idade} anos)"
    
    def __repr__(self):
        """Representação para desenvolvedor"""
        return f"Pessoa('{self.nome}', {self.idade})"

p = Pessoa("João Silva", 25)
print(p)                    # João Silva (25 anos) — chama __str__
print(repr(p))              # Pessoa('João Silva', 25) — chama __repr__

__len__: Fazendo Objetos Suportarem len()

PYTHON
class Time:
    def __init__(self, membros=None):
        self.membros = membros or []
    
    def add(self, membro):
        self.membros.append(membro)
    
    def __len__(self):
        return len(self.membros)
    
    def __str__(self):
        return f"Time({', '.join(self.membros)})"

time = Time()
time.add("João Silva")
time.add("Maria Souza")
time.add("Pedro Santos")

print(len(time))            # 3 — chama __len__
print(time)                 # Time(João Silva, Maria Souza, Pedro Santos)

__getitem__: Fazendo Objetos Suportarem Indexação

PYTHON
class Playlist:
    def __init__(self):
        self.musicas = []
    
    def add(self, musica):
        self.musicas.append(musica)
    
    def __getitem__(self, index):
        return self.musicas[index]
    
    def __len__(self):
        return len(self.musicas)

playlist = Playlist()
playlist.add("Dia Ensolarado")
playlist.add("Fragrância das Sete Léguas")
playlist.add("Noturno")

print(playlist[0])          # Dia Ensolarado — chama __getitem__
print(len(playlist))        # 3

# Suporta fatiamento e iteração
for musica in playlist:
    print(musica, end=" ")    # Dia Ensolarado Fragrância das Sete Léguas Noturno

3. Referência de Métodos Mágicos Comuns

PYTHON
class Demo:
    def __init__(self, valor):      # Construtor
        self.valor = valor
    
    def __str__(self):              # str() / print()
        return str(self.valor)
    
    def __repr__(self):             # repr() / depuração
        return f"Demo({self.valor})"
    
    def __len__(self):              # len()
        return 1
    
    def __bool__(self):             # bool() / condição if
        return self.valor != 0
    
    def __eq__(self, outro):        # ==
        return self.valor == outro.valor
    
    def __lt__(self, outro):        # < (para ordenação)
        return self.valor < outro.valor
    
    def __add__(self, outro):       # operador +
        return Demo(self.valor + outro.valor)
    
    def __getitem__(self, chave):   # obj[chave]
        return chave

Exemplo: Classe Fração (Dificuldade ⭐⭐⭐)

PYTHON
class Fracao:
    """Classe Fração — suporta adição"""
    def __init__(self, numerador, denominador=1):
        if denominador == 0:
            raise ValueError("Denominador não pode ser zero")
        self.numerador = numerador
        self.denominador = denominador
    
    def __str__(self):
        if self.denominador == 1:
            return str(self.numerador)
        return f"{self.numerador}/{self.denominador}"
    
    def __add__(self, outro):
        novo_den = self.denominador * outro.denominador
        novo_num = self.numerador * outro.denominador + outro.numerador * self.denominador
        return Fracao(novo_num, novo_den)
    
    def __eq__(self, outro):
        return (self.numerador * outro.denominador) == (outro.numerador * self.denominador)

f1 = Fracao(1, 3)
f2 = Fracao(1, 6)
print(f1)                           # 1/3
print(f1 + f2)                      # 9/18
print(Fracao(1, 2) == Fracao(2, 4))  # True
▶ Experimente

4. @dataclass: Definindo Rapidamente Classes de Dados

Escrever uma classe apenas para armazenar dados, com __init__, __repr__, __eq__ manuais, é tedioso. @dataclass os gera automaticamente:

PYTHON
from dataclasses import dataclass

@dataclass
class Ponto:
    x: float
    y: float

@dataclass
class Aluno:
    nome: str
    idade: int
    nota: float = 0.0    # Valor padrão

# Obtém automaticamente __init__, __repr__, __eq__
p1 = Ponto(3.0, 4.0)
p2 = Ponto(3.0, 4.0)
print(p1)                   # Ponto(x=3.0, y=4.0)
print(p1 == p2)             # True (compara todos os campos automaticamente)

s = Aluno("João Silva", 20, 85.5)
print(s)                    # Aluno(nome='João Silva', idade=20, nota=85.5)
💡 Quando usar @dataclass: 1) Uma classe que armazena principalmente dados com poucos métodos. 2) Você precisa de código boilerplate como __init__, __repr__, __eq__. 3) Você não quer escrever isso manualmente. Para casos simples, é muito mais eficiente que escrever classes manualmente.


Casos de Uso Comuns


❓ Perguntas Frequentes

P: Qual a diferença entre métodos mágicos e métodos regulares? R: Métodos mágicos são chamados por operações embutidas do Python — print() chama __str__, len() chama __len__, == chama __eq__. Você não os chama diretamente; apenas os implementa, e Python os chama automaticamente no momento certo. P: Qual a diferença entre @dataclass e uma classe regular? R: @dataclass gera automaticamente os métodos que você está cansado de escrever. É apenas um gerador de código — a classe gerada é idêntica a uma escrita manualmente. Classes decoradas com @dataclass também podem ter seus próprios métodos. Se você se pega repetidamente escrevendo __init__ para armazenar propriedades, é hora de usar @dataclass. P: Python tem tipagem dinâmica — o polimorfismo vem naturalmente? R: Sim. O "duck typing" do Python torna o polimorfismo possível sem herança — apenas ter métodos com o mesmo nome é suficiente. Outras linguagens (como Java/C++) precisam de interfaces ou classes abstratas para polimorfismo. O benefício é a flexibilidade; a desvantagem é que a verificação de tipo não pode capturar erros tão facilmente.


📖 Resumo


📝 Atividades

  1. Iniciante (Dificuldade ⭐): Defina uma classe Livro implementando __str__ e __len__ (retornando o número de páginas). Crie dois livros e exiba print(livro) e len(livro).

  2. Intermediário (Dificuldade ⭐⭐): Use @dataclass para definir uma classe Produto (nome, preço, quantidade). Implemente __add__ — mescle dois produtos com o mesmo nome (some quantidades, calcule preço médio). Dica: Implemente dentro da classe @dataclass.

  3. Avançado (Dificuldade ⭐⭐⭐): Defina uma classe Baralho (um baralho de cartas) implementando __len__, __getitem__ (suportando indexação e fatiamento) e embaralhar() (usando random.shuffle). Depois escreva código para criar um baralho e distribuir as primeiras 5 cartas como uma mão.

100%