W3docs

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 type

Isso é 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() method

Adicionar 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çãoAbordagem 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 comportamentoDuck typing
Você quer impor a interface no momento da definiçãoClasses base abstratas
Trabalhando com tipos embutidos ou classes de terceiros que você não pode alterarDuck 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.00

O 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.00

Se 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, perimeter

As 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))  # True

Armadilha 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

ConceitoO que significa
Sobrescrita de métodosUma subclasse fornece sua própria versão de um método pai
Despacho dinâmicoPython escolhe a versão correta do método em tempo de execução
Duck typingQualquer objeto com os métodos corretos funciona, independentemente de sua classe
Sobrecarga de operadoresMétodos dunder (__add__, __len__, …) tornam os operadores polimórficos
Classes base abstratasImpõ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.

Prática

Prática
Which of the following statements about Python polymorphism are correct?
Which of the following statements about Python polymorphism are correct?
Was this page helpful?