Herança e Derivação em Programação Orientada a Objetos com Python

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.

Tags: Python POO Herança Classes Abstratas mro

Publicado em 7-13 09:36