Polimorfismo em Python
Aprenda polimorfismo em Python: sobrescrita de métodos, duck typing, sobrecarga de operadores e interfaces abstratas — com exemplos claros.
Polimorfismo (do grego "muitas formas") permite que um único trecho de código funcione com objetos de diferentes tipos, desde que esses objetos suportem a interface esperada. Você chama o mesmo nome de método em cada objeto, e cada objeto responde de acordo com sua própria implementação.
O polimorfismo é um dos quatro pilares da programação orientada a objetos, ao lado de encapsulamento, herança e abstração. É o que faz com que funções que aceitam um tipo de classe base funcionem automaticamente com qualquer subclasse.
Este capítulo aborda:
- O que é polimorfismo e por que ele é importante
- Polimorfismo através da sobrescrita de métodos
- Polimorfismo através do duck typing
- Sobrecarga de operadores — uma forma de polimorfismo integrada ao Python
- Classes base abstratas como uma maneira formal de definir interfaces polimórficas
- Padrões práticos e armadilhas comuns
Antes de ler este capítulo, certifique-se de que você está familiarizado com classes e objetos em Python e herança em Python.
Por que o Polimorfismo é Importante
Sem polimorfismo, uma função que trabalha com animais precisaria de uma cadeia explícita de if/elif para cada tipo de animal:
def make_sound(animal):
if type(animal).__name__ == "Dog":
print("Woof!")
elif type(animal).__name__ == "Cat":
print("Meow!")
elif type(animal).__name__ == "Bird":
print("Tweet!")
# ... add a new branch every time you add a new animal typeIsso é frágil. Cada novo tipo de animal exige a modificação dessa função. Com polimorfismo, você escreve:
def make_sound(animal):
animal.speak() # works for any object that has a speak() methodAdicionar um novo tipo de animal requer apenas definir seu método speak() — a função em si nunca muda. Este é o princípio aberto/fechado: aberto para extensão, fechado para modificação.
Polimorfismo Através da Sobrescrita de Métodos
A forma mais comum de polimorfismo em Python é a sobrescrita de métodos: uma subclasse fornece sua própria versão de um método que foi definido pela classe pai.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
class Dog(Animal):
def speak(self):
return f"{self.name} says woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says meow!"
class Bird(Animal):
def speak(self):
return f"{self.name} says tweet!"Agora um único laço funciona para os três tipos:
animals = [Dog("Rex"), Cat("Whiskers"), Bird("Tweety")]
for animal in animals:
print(animal.speak())
# Rex says woof!
# Whiskers says meow!
# Tweety says tweet!animal.speak() despacha para a versão correta em tempo de execução com base no tipo real do objeto. Esse despacho em tempo de execução é chamado de despacho dinâmico ou ligação tardia.
Estender Versus Substituir o Método Pai
Ao sobrescrever, você pode substituir completamente o comportamento do pai ou estendê-lo usando super():
class Animal:
def speak(self):
print("[Animal vocalization]")
class Dog(Animal):
def speak(self):
super().speak() # keep the parent's output
print("Woof! (Dog override adds this)")
Dog().speak()
# [Animal vocalization]
# Woof! (Dog override adds this)Use super() quando a versão do pai realiza uma configuração útil ou registro de log que ainda deve ser executado. Omita quando quiser substituir o comportamento completamente.
Polimorfismo Através do Duck Typing
O sistema de tipos do Python é estrutural em vez de nominal. Um objeto não precisa pertencer a uma hierarquia de classes específica; ele apenas precisa ter os métodos corretos. Isso é chamado de duck typing — inspirado no ditado "se anda como um pato e grasna como um pato, então é um pato".
class Dog:
def speak(self):
return "Woof!"
class Robot:
def speak(self):
return "Beep boop."
class Human:
def speak(self):
return "Hello!"
def introduce(entity):
print(entity.speak())
introduce(Dog()) # Woof!
introduce(Robot()) # Beep boop.
introduce(Human()) # Hello!Dog, Robot e Human não compartilham nenhuma classe pai comum (além do object embutido). Mesmo assim, introduce() funciona com os três porque cada um tem um método speak(). A função não verifica qual é o tipo de entity — ela simplesmente chama o método e confia que o objeto responderá corretamente.
Quando Usar Duck Typing Versus Herança
| Situação | Abordagem preferida |
|---|---|
| Os objetos são logicamente relacionados (todos são animais) | Hierarquia de herança |
| Os objetos são não relacionados, mas compartilham um comportamento | Duck typing |
| Você quer impor a interface no momento da definição | Classes base abstratas |
| Trabalhando com tipos embutidos ou classes de terceiros que você não pode alterar | Duck typing |
Duck typing é idiomático em Python e é amplamente usado na biblioteca padrão — por exemplo, len() funciona em qualquer objeto que defina __len__, independentemente de sua classe.
Polimorfismo com Funções e Laços
Você pode escrever uma única função que trata diferentes tipos de forma uniforme através do polimorfismo. Considere uma aplicação de desenho:
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def describe(self):
return f"Circle with radius {self.radius}"
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def describe(self):
return f"Rectangle {self.width}x{self.height}"
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
def describe(self):
return f"Triangle base={self.base} height={self.height}"
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]
for shape in shapes:
print(f"{shape.describe()}: area = {shape.area():.2f}")
# Circle with radius 5: area = 78.54
# Rectangle 4x6: area = 24.00
# Triangle base=3 height=8: area = 12.00O laço chama area() e describe() em cada forma sem se importar a qual classe o objeto pertence. Adicionar uma classe Pentagon posteriormente requer apenas escrever a nova classe — o laço não muda.
Sobrecarga de Operadores
Os operadores aritméticos e de comparação do Python também são polimórficos. + em inteiros soma números; + em strings os concatena; + em listas os mescla. Python consegue isso por meio de métodos especiais (dunder).
Você pode fazer suas próprias classes responderem a operadores definindo esses métodos:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 * 3) # Vector(3, 6)O mesmo operador + agora se comporta de forma diferente dependendo se os operandos são inteiros, strings ou objetos Vector. Isso é polimorfismo no nível do operador.
Para um mergulho profundo nos métodos especiais do Python, veja Métodos Mágicos do Python.
Polimorfismo com Classes Base Abstratas
Classes base abstratas (ABCs) levam o duck typing um passo adiante ao impor a interface no momento da definição da classe. Se uma subclasse não implementar um método obrigatório, o Python lança um TypeError no momento em que você tenta instanciá-la.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
"""Return the area of the shape."""
@abstractmethod
def perimeter(self) -> float:
"""Return the perimeter of the shape."""
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
def perimeter(self) -> float:
import math
return 2 * math.pi * self.radius
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2
def perimeter(self) -> float:
return 4 * self.side
def print_info(shape: Shape) -> None:
print(f"Area: {shape.area():.2f}")
print(f"Perimeter: {shape.perimeter():.2f}")
print_info(Circle(5))
# Area: 78.54
# Perimeter: 31.42
print_info(Square(4))
# Area: 16.00
# Perimeter: 16.00Se você tentar instanciar uma subclasse que não implementa area():
class Blob(Shape):
pass # forgot to implement area() and perimeter()
b = Blob()
# TypeError: Can't instantiate abstract class Blob with abstract methods area, perimeterAs ABCs fornecem a rede de segurança de um contrato formal enquanto ainda permitem polimorfismo em tempo de execução.
ABCs Versus Duck Typing — Qual Deve Usar?
- Duck typing é mais simples e flexível. Prefira-o para bases de código internas pequenas e scripts.
- ABCs tornam a interface esperada explícita, detectam bugs de método faltante mais cedo e aparecem no autocompletar de IDEs e verificadores de tipos. Prefira-as em bases de código maiores, bibliotecas públicas e em qualquer lugar onde você queira impor um contrato.
Um Exemplo do Mundo Real: Processamento de Pagamentos
O polimorfismo brilha em designs de estilo plugin. Considere um sistema de pagamento que precisa suportar múltiplos provedores:
from abc import ABC, abstractmethod
class PaymentProvider(ABC):
@abstractmethod
def charge(self, amount: float, currency: str) -> bool:
"""Attempt to charge the given amount. Return True on success."""
@abstractmethod
def refund(self, transaction_id: str) -> bool:
"""Refund a previous transaction. Return True on success."""
class StripeProvider(PaymentProvider):
def charge(self, amount: float, currency: str) -> bool:
print(f"[Stripe] Charged {amount} {currency}")
return True
def refund(self, transaction_id: str) -> bool:
print(f"[Stripe] Refunded transaction {transaction_id}")
return True
class PayPalProvider(PaymentProvider):
def charge(self, amount: float, currency: str) -> bool:
print(f"[PayPal] Charged {amount} {currency}")
return True
def refund(self, transaction_id: str) -> bool:
print(f"[PayPal] Refunded transaction {transaction_id}")
return True
def process_order(provider: PaymentProvider, amount: float) -> None:
success = provider.charge(amount, "USD")
if success:
print("Order complete.")
process_order(StripeProvider(), 99.99)
# [Stripe] Charged 99.99 USD
# Order complete.
process_order(PayPalProvider(), 49.5)
# [PayPal] Charged 49.5 USD
# Order complete.process_order não sabe nem se importa se recebe um StripeProvider ou um PayPalProvider. Adicionar um novo CryptoProvider requer apenas escrever a nova classe — nada mais muda. Isso é polimorfismo entregando extensibilidade no mundo real.
Armadilhas Comuns
Armadilha 1: Verificar Tipos com type() em Vez de isinstance()
Comparar type(obj) == Dog derrota o polimorfismo porque retorna False para subclasses. Prefira isinstance(obj, Animal), que retorna True tanto para Dog quanto para qualquer subclasse futura:
class Animal:
pass
class Dog(Animal):
pass
d = Dog()
# Fragile — breaks for subclasses:
print(type(d) == Animal) # False
# Correct — subclass-aware:
print(isinstance(d, Animal)) # TrueArmadilha 2: Assinaturas de Métodos Inconsistentes
O polimorfismo pressupõe que todas as implementações de um método aceitam os mesmos argumentos. Se Dog.speak() requer um argumento que Cat.speak() não requer, os chamadores que os tratam de forma uniforme vão falhar:
# Inconsistent — will cause errors in a loop
class Dog:
def speak(self, volume): # extra argument!
return f"Woof at volume {volume}"
class Cat:
def speak(self):
return "Meow!"Mantenha as assinaturas de métodos consistentes entre classes polimórficas.
Armadilha 3: Argumentos Padrão Mutáveis em Métodos Sobrescritos
Esta é uma armadilha mais ampla do Python, mas ela pega as pessoas em hierarquias de classes: nunca use um padrão mutável (lista, dict) como valor de argumento padrão — ele é criado uma vez e compartilhado entre todas as chamadas.
# Bug: the list is shared across all instances
class Item:
def __init__(self, tags=[]): # BAD
self.tags = tags
# Fix:
class Item:
def __init__(self, tags=None):
self.tags = tags if tags is not None else []Resumo
| Conceito | O que significa |
|---|---|
| Sobrescrita de métodos | Uma subclasse fornece sua própria versão de um método pai |
| Despacho dinâmico | Python escolhe a versão correta do método em tempo de execução |
| Duck typing | Qualquer objeto com os métodos corretos funciona, independentemente de sua classe |
| Sobrecarga de operadores | Métodos dunder (__add__, __len__, …) tornam os operadores polimórficos |
| Classes base abstratas | Impõem formalmente a interface que as subclasses devem implementar |
O polimorfismo é o que torna o código extensível sem modificação. Escreva funções que dependam de comportamento (nomes de métodos), não de tipos concretos, e seu código acomodará naturalmente novas classes sem alterações.