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:
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:
Au au!
Miau!
Quack!
Exemplo: Cálculo de Área de Formas (Dificuldade ⭐⭐)
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}")
Saída:
Á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
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()
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
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
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 ⭐⭐⭐)
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
4. @dataclass: Definindo Rapidamente Classes de Dados
Escrever uma classe apenas para armazenar dados, com __init__, __repr__, __eq__ manuais, é tedioso. @dataclass os gera automaticamente:
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)
@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
- Duck typing:
len(),for...in,sorted()do Python são todos baseados em protocolo — implemente o método correspondente e sua classe pode usá-los. - Containers de dados: Implemente
__getitem__+__len__para fazer sua classe suportar indexação e iteração. - Sobrecarga de operadores:
__add__,__eq__,__lt__permitem que classes personalizadas suportem+,==,<etc. - Classes de dados:
@dataclasspara definir rapidamente configurações, DTOs, objetos de valor.
❓ 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@dataclasse uma classe regular? R:@dataclassgera 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@dataclasstambé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
- Polimorfismo: objetos de diferentes classes com o mesmo nome de método podem ser chamados da mesma forma
- Duck typing: "Se faz quack, é um pato" — importa os métodos, não os tipos
- Métodos mágicos:
__str__(impressão),__len__(comprimento),__getitem__(indexação),__eq__(igualdade),__add__(adição), etc. @dataclassgera automaticamente__init__,__repr__,__eq__— ideal para classes de dados puras
📝 Atividades
-
Iniciante (Dificuldade ⭐): Defina uma classe
Livroimplementando__str__e__len__(retornando o número de páginas). Crie dois livros e exibaprint(livro)elen(livro). -
Intermediário (Dificuldade ⭐⭐): Use
@dataclasspara definir uma classeProduto(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. -
Avançado (Dificuldade ⭐⭐⭐): Defina uma classe
Baralho(um baralho de cartas) implementando__len__,__getitem__(suportando indexação e fatiamento) eembaralhar()(usandorandom.shuffle). Depois escreva código para criar um baralho e distribuir as primeiras 5 cartas como uma mão.



