Introdução à Herança
Herança é um mecanismo que permite criar novas classes com base em classes existentes, promovendo a reutilização de código. Em Python, é possível herdar de uma ou múltiplas classes (herança múltipla). A classe original é conhecida como classe base ou superclasse, e a nova classe como subclasse ou classe derivada.
class ClassePai1:
pass
class ClassePai2:
pass
class SubClasse1(ClassePai1): # Herança simples
pass
class SubClasse2(ClassePai1, ClassePai2): # Herança múltipla
pass
Para inspecionar as classes base de uma subclasse, utilize os atributos especiais __bases__ e __base__. O primeiro retorna todas as classes base, enquanto o segundo retorna apenas a primeira classe na lista de herança.
print(SubClasse1.__bases__) # Saída: (<class '__main__.ClassePai1'>,)
print(SubClasse2.__bases__) # Saída: (<class '__main__.ClassePai1'>, <class '__main__.ClassePai2'>)
No Python 3, todas as classes são "novas" por padrão, herdando implicitamente de object, a raiz da hierarquia de classes. Em Python 2, havia distinção entre classes clássicas e novas.
print(ClassePai1.__bases__) # Saída: (<class 'object'>,)
Herança e Abstração
A herança descreve uma relação "é um", como "um gato é um animal". Para estabelecer essa relação, é necessário primeiro abstrair características comuns. A abstração envolve identificar similaridades entre objetos ou classes para criar uma hierarquia lógica.
Dois níveis de abstração são consiedrados: extrair classes de objetos semelhantes (ex.: humanos) e extrair uma superclasse de classes com atributos comuns (ex.: animais). A herança implementa essa estrutura abstrata no código.
Herança e Reutilização de Código
A herança permite reutilizar código, evitando duplicação. Considere um exemplo com animais: em vez de definir métodos comuns separadamente para cada espécie, crie uma classe base Animal com métodos como comer e beber, e subclasses específicas.
class Animal:
def comer(self):
print(f"{self.nome} está comendo")
def beber(self):
print(f"{self.nome} está bebendo")
class Gato(Animal):
def __init__(self, nome):
self.nome = nome
def miar(self):
print("Miau!")
class Cachorro(Animal):
def __init__(self, nome):
self.nome = nome
def latir(self):
print("Au au!")
# Uso
gato = Gato("Felix")
gato.comer() # Saída: Felix está comendo
gato.miar() # Saída: Miau!
cachorro = Cachorro("Rex")
cachorro.beber() # Saída: Rex está bebendo
Ao buscar atributos, Python segue a ordem: instância, classe, classes base na hierarquia. Isso é conhecido como resolução de atributos.
class ExemploBase:
def metodo(self):
print("Método na classe base")
class ExemploDerivado(ExemploBase):
def metodo_derivado(self):
self.metodo() # Chama o método da classe base
obj = ExemploDerivado()
obj.metodo_derivado() # Saída: Método na classe base
Derivação
Subclasses podem adicionar novos atributos ou sobrescrever métodos da classe base. Quando um método é redefinido, a implementação da subclasse prevalece. Para chamar o método da classe base dentro da subclasse, use NomeDaClasse.método(self, ...).
class Veiculo:
def __init__(self, modelo, velocidade):
self.modelo = modelo
self.velocidade = velocidade
def mover(self):
print(f"{self.modelo} está em movimento")
class Carro(Veiculo):
def __init__(self, modelo, velocidade, combustivel):
Veiculo.__init__(self, modelo, velocidade) # Chamando o construtor da classe base
self.combustivel = combustivel
def mover(self):
Veiculo.mover(self) # Reutilizando a lógica da classe base
print(f"Usando {self.combustivel}")
meu_carro = Carro("Sedan", 120, "gasolina")
meu_carro.mover()
# Saída:
# Sedan está em movimento
# Usando gasolina
Composição e Reutilização
A composição é outra técnica de reutilização, onde uma classe contém instâncias de outras classes como atributos, representando uma relação "tem um". É adequada quando classes têm responsabilidades distintas.
class Motor:
def ligar(self):
print("Motor ligado")
class Carro:
def __init__(self, modelo):
self.modelo = modelo
self.motor = Motor() # Composição: Carro tem um Motor
def iniciar(self):
self.motor.ligar()
print(f"{self.modelo} pronto para dirigir")
meu_carro = Carro("Esportivo")
meu_carro.iniciar()
# Saída:
# Motor ligado
# Esportivo pronto para dirigir
Use herança para relações "é um" (ex.: professor é uma pessoa) e composição para relações "tem um" (ex.: professor tem disciplinas).
Interfaces e Design de Interface Única
Uma interface define um contrato de métodos que as clases devem implementar. Em Python, isso pode ser simulado com classes abstratas ou módulos como abc. A padronização de interfaces permite tratar objetos de diferentes classes de maneira uniforme.
from abc import ABC, abstractmethod
class InterfaceLeitura(ABC):
@abstractmethod
def ler(self):
pass
@abstractmethod
def escrever(self):
pass
class ArquivoTexto(InterfaceLeitura):
def ler(self):
print("Lendo arquivo de texto")
def escrever(self):
print("Escrevendo em arquivo de texto")
class BancoDados(InterfaceLeitura):
def ler(self):
print("Lendo dados do banco")
def escrever(self):
print("Escrevendo no banco de dados")
# Uso polimórfico
def processar_armazenamento(objeto):
objeto.ler()
objeto.escrever()
texto = ArquivoTexto()
banco = BancoDados()
processar_armazenamento(texto)
processar_armazenamento(banco)
Classes Abstratas
Classes abstratas não podem ser instanciadas e servem como modelos para subclasses. Elas podem conter métodos abstrtaos que devem ser implementados pelas subclasses, garantindo uma estrutura consistente.
from abc import ABC, abstractmethod
class FiguraGeometrica(ABC):
@abstractmethod
def calcular_area(self):
pass
class Circulo(FiguraGeometrica):
def __init__(self, raio):
self.raio = raio
def calcular_area(self):
return 3.14 * self.raio ** 2
class Retangulo(FiguraGeometrica):
def __init__(self, largura, altura):
self.largura = largura
self.altura = altura
def calcular_area(self):
return self.largura * self.altura
# Tentar instanciar FiguraGeometrica causaria erro
circulo = Circulo(5)
retangulo = Retangulo(4, 6)
print(circulo.calcular_area()) # Saída: 78.5
print(retangulo.calcular_area()) # Saída: 24
Princípios de Implementação de Herança (Problema do Diamante)
Em herança múltipla, a ordem de resolução de métodos (MRO) determina qual classe é consultada primeiro. Python usa o algoritmo C3 Linearização para garantir uma ordem consistente.
class A:
def metodo(self):
print("A.metodo")
class B(A):
def metodo(self):
print("B.metodo")
class C(A):
def metodo(self):
print("C.metodo")
class D(B, C):
pass
obj = D()
obj.metodo() # Saída: B.metodo
# Verificar MRO
print(D.__mro__)
# Saída: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
A MRO segue três regras: subclasses antes de classes base, ordem de listagem respeitada e escolha da primeira opção válida em caso de conflito.
Chamando Métodos da Classe Pai em Subclasses
Existem duas formas de chamar métodos da classe base: referenciando explicitamente o nome da classe ou usando super(). A segunda é recomendada para herança múltipla, pois segue a MRO.
class Base:
def __init__(self, valor):
self.valor = valor
print("Construtor Base chamado")
class Derivada(Base):
def __init__(self, valor, extra):
Base.__init__(self, valor) # Forma explícita
self.extra = extra
print("Construtor Derivada chamado")
class DerivadaComSuper(Base):
def __init__(self, valor, extra):
super().__init__(valor) # Forma com super()
self.extra = extra
# Exemplo com super() e herança múltipla
class X:
def metodo(self):
print("X.metodo")
class Y:
def metodo(self):
print("Y.metodo")
class Z(X, Y):
def metodo(self):
super().metodo() # Segue MRO: X primeiro, depois Y
z = Z()
z.metodo() # Saída: X.metodo
print(Z.__mro__)
# Saída: (<class '__main__.Z'>, <class '__main__.X'>, <class '__main__.Y'>, <class 'object'>)
O uso de super() assegura que cada método seja chamado apenas uma vez, respeitando a hierarquia definida pela MRO.