Voltar ao Glossario
Glossario Python

Herança em Python: O que É e Como Funciona | Python Brasil

Entenda herança em Python: MRO, C3 linearization, super(), mixins, classes abstratas e composição vs herança. Exemplos práticos completos.

O que é Herança?

Herança é um dos pilares da Programação Orientada a Objetos que permite que uma classe herde atributos e métodos de outra classe. A classe que herda é chamada de classe filha (ou subclasse), e a classe da qual se herda é a classe pai (ou superclasse).

Isso promove a reutilização de código e estabelece relações hierárquicas entre classes. Herança deve modelar uma relação clara de “é um”: um Cachorro é um Animal. Se a relação for “tem um”, prefira composição.

Herança simples

class Animal:
    def __init__(self, nome, idade):
        self.nome = nome
        self.idade = idade

    def falar(self):
        raise NotImplementedError("Subclasses devem implementar este método")

    def info(self):
        return f"{self.nome}, {self.idade} anos"

class Cachorro(Animal):
    def __init__(self, nome, idade, raca):
        super().__init__(nome, idade)  # Chama o construtor da classe pai
        self.raca = raca

    def falar(self):
        return "Au au!"

    def info(self):
        base = super().info()  # Reutiliza o método da classe pai
        return f"{base}, raça: {self.raca}"

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

rex = Cachorro("Rex", 5, "Labrador")
mimi = Gato("Mimi", 3)

print(rex.info())              # Rex, 5 anos, raça: Labrador
print(rex.falar())             # Au au!
print(mimi.falar())            # Miau!
print(isinstance(rex, Animal)) # True

MRO: Ordem de Resolução de Métodos

Quando Python precisa encontrar um atributo ou método, ele segue a Method Resolution Order (MRO). Em herança simples isso é direto, mas em herança múltipla o Python usa o algoritmo C3 Linearization para determinar a ordem de forma consistente e previsível.

class A:
    def metodo(self):
        return "A"

class B(A):
    def metodo(self):
        return f"B -> {super().metodo()}"

class C(A):
    def metodo(self):
        return f"C -> {super().metodo()}"

class D(B, C):
    def metodo(self):
        return f"D -> {super().metodo()}"

print(D.__mro__)
# D -> B -> C -> A -> object

print(D().metodo())
# D -> B -> C -> A

O C3 Linearization garante que: a classe filha sempre aparece antes das classes pai; a ordem das classes pai é preservada; nenhuma classe aparece antes de suas dependências. Você pode inspecionar o MRO de qualquer classe com Classe.__mro__ ou Classe.mro().

super() em profundidade

super() não retorna “a classe pai” — ele retorna um proxy que segue o MRO da classe corrente. Isso é fundamental em herança múltipla:

class Logavel:
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        print(f"Logavel inicializado")

class Serializavel:
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        print(f"Serializavel inicializado")

class Base:
    def __init__(self, nome, **kwargs):
        super().__init__(**kwargs)
        self.nome = nome
        print(f"Base inicializado com nome={nome}")

class Entidade(Logavel, Serializavel, Base):
    def __init__(self, nome, **kwargs):
        super().__init__(nome=nome, **kwargs)
        print("Entidade inicializada")

e = Entidade("Produto")
# Base inicializado com nome=Produto
# Serializavel inicializado
# Logavel inicializado
# Entidade inicializada

Note o uso de **kwargs para que cada classe no MRO passe os argumentos restantes adiante sem quebrar a cadeia de chamadas.

Herança múltipla e o padrão Mixin

O padrão Mixin é uma das formas mais elegantes de usar herança múltipla. Um mixin é uma classe que fornece um conjunto específico de funcionalidades, mas que não é destinada a ser instanciada sozinha.

class JsonMixin:
    """Mixin que adiciona serialização JSON a qualquer classe."""
    def to_json(self):
        import json
        return json.dumps(self.__dict__, ensure_ascii=False, default=str)

    @classmethod
    def from_json(cls, json_str):
        import json
        dados = json.loads(json_str)
        return cls(**dados)

class LogMixin:
    """Mixin que adiciona logging automático a métodos."""
    def log(self, mensagem):
        import logging
        logging.info(f"[{self.__class__.__name__}] {mensagem}")

class ValidacaoMixin:
    """Mixin que adiciona validação de campos obrigatórios."""
    campos_obrigatorios = []

    def validar(self):
        for campo in self.campos_obrigatorios:
            if not getattr(self, campo, None):
                raise ValueError(f"Campo obrigatório ausente: {campo}")
        return True

class Usuario(JsonMixin, LogMixin, ValidacaoMixin):
    campos_obrigatorios = ['nome', 'email']

    def __init__(self, nome, email, idade=None):
        self.nome = nome
        self.email = email
        self.idade = idade

u = Usuario("Ana", "ana@email.com", 28)
print(u.to_json())   # {"nome": "Ana", "email": "ana@email.com", "idade": 28}
u.validar()          # True
u.log("Usuário criado com sucesso")

Classes abstratas como contratos

O módulo abc permite criar classes que definem uma interface obrigatória para suas subclasses:

from abc import ABC, abstractmethod

class Repositorio(ABC):
    """Define o contrato que todos os repositórios devem cumprir."""

    @abstractmethod
    def buscar_por_id(self, id: int):
        pass

    @abstractmethod
    def salvar(self, entidade) -> None:
        pass

    @abstractmethod
    def deletar(self, id: int) -> None:
        pass

    def buscar_todos(self):  # Método concreto — opcional sobrescrever
        return []

class RepositorioEmMemoria(Repositorio):
    def __init__(self):
        self._dados = {}

    def buscar_por_id(self, id):
        return self._dados.get(id)

    def salvar(self, entidade):
        self._dados[entidade['id']] = entidade

    def deletar(self, id):
        self._dados.pop(id, None)

# Repositorio()  # TypeError: não pode instanciar classe abstrata
repo = RepositorioEmMemoria()
repo.salvar({'id': 1, 'nome': 'Produto A'})
print(repo.buscar_por_id(1))  # {'id': 1, 'nome': 'Produto A'}

Composição vs herança

A herança muitas vezes é usada em excesso. Antes de criar uma hierarquia de herança, pergunte-se: a relação é realmente “é um”?

# Herança: "Motor É um Carro?" — Não faz sentido!
class Carro(Motor):  # Errado
    pass

# Composição: "Carro TEM um Motor" — Correto
class Motor:
    def __init__(self, potencia):
        self.potencia = potencia

    def ligar(self):
        return f"Motor de {self.potencia}cv ligado"

class Carro:
    def __init__(self, modelo, potencia):
        self.modelo = modelo
        self.motor = Motor(potencia)  # Composição

    def ligar(self):
        return f"{self.modelo}: {self.motor.ligar()}"

c = Carro("Fusca", 65)
print(c.ligar())  # Fusca: Motor de 65cv ligado

A composição favorece o Princípio da Responsabilidade Única e torna o código mais flexível para mudanças futuras. Você pode trocar o motor sem alterar a classe Carro.

Princípio de Substituição de Liskov (LSP)

O LSP afirma que objetos de uma subclasse devem poder substituir objetos da superclasse sem quebrar o programa. Violações comuns ocorrem quando a subclasse restringe o comportamento esperado:

# Violação do LSP — Quadrado "quebrando" o contrato de Retangulo
class Retangulo:
    def __init__(self, largura, altura):
        self.largura = largura
        self.altura = altura

    def area(self):
        return self.largura * self.altura

class Quadrado(Retangulo):  # Problema: quadrado viola o contrato
    def __init__(self, lado):
        super().__init__(lado, lado)

    @Retangulo.largura.setter
    def largura(self, valor):
        self._largura = valor
        self._altura = valor  # Força lado igual — quebra o contrato!

# Solução LSP: ambos herdam de uma abstração comum
class Forma(ABC):
    @abstractmethod
    def area(self): pass

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

class Quadrado(Forma):
    def __init__(self, lado):
        self.lado = lado
    def area(self):
        return self.lado ** 2

Exemplo prático: refatorando com herança

# Antes: código duplicado em cada classe de notificação
class NotificacaoEmail:
    def enviar(self, destinatario, mensagem):
        print(f"[LOG] Enviando email para {destinatario}")
        # lógica de email...
        print(f"[LOG] Email enviado")

class NotificacaoSMS:
    def enviar(self, destinatario, mensagem):
        print(f"[LOG] Enviando SMS para {destinatario}")
        # lógica de SMS...
        print(f"[LOG] SMS enviado")

# Depois: herança elimina duplicação
class Notificacao(ABC):
    def enviar(self, destinatario, mensagem):
        print(f"[LOG] Enviando {self.__class__.__name__} para {destinatario}")
        self._executar_envio(destinatario, mensagem)
        print(f"[LOG] {self.__class__.__name__} enviado")

    @abstractmethod
    def _executar_envio(self, destinatario, mensagem):
        pass

class NotificacaoEmail(Notificacao):
    def _executar_envio(self, destinatario, mensagem):
        print(f"Email: {mensagem} -> {destinatario}")

class NotificacaoSMS(Notificacao):
    def _executar_envio(self, destinatario, mensagem):
        print(f"SMS: {mensagem} -> {destinatario}")

Erros comuns

Chamar super() sem argumentos funciona apenas dentro de métodos de classe. Em Python 2 era necessário passar a classe e self explicitamente — em Python 3 isso é desnecessário. Outro erro comum é não usar super().__init__() na subclasse quando a superclasse possui inicialização, o que resulta em atributos ausentes.

Quando usar herança?

Use herança quando: a subclasse é um tipo da superclasse; você quer reutilizar e estender o comportamento; as classes compartilham uma interface comum obrigatória. Evite herança apenas para reutilização de código — para isso, prefira composição ou mixins bem delimitados.

Termos Relacionados