ABC e classes abstratas

1. Introdução às Classes Abstratas

Classes abstratas são um conceito fundamental no design orientado a objetos que estabelece contratos entre classes. Diferentemente das classes concretas, que podem ser instanciadas diretamente, as classes abstratas servem como modelos para outras classes, definindo uma interface que as subclasses devem implementar.

Em Python, as classes abstratas desempenham um papel crucial na criação de código polimórfico e padronizado. Elas permitem que desenvolvedores definam métodos que obrigatoriamente devem ser implementados pelas subclasses, garantindo que todas as classes derivadas sigam o mesmo contrato. Isso é especialmente útil em projetos grandes e bibliotecas, onde a consistência da interface é essencial.

A principal diferença entre classes concretas e abstratas está na capacidade de instanciação: enquanto você pode criar objetos de classes concretas, tentar instanciar uma classe abstrata resultará em erro. Além disso, classes abstratas podem conter tanto métodos abstratos (sem implementação) quanto métodos concretos (com implementação).

2. O Módulo abc e a Classe ABC

Para trabalhar com classes abstratas em Python, utilizamos o módulo abc (Abstract Base Classes). A classe base ABC fornecida por este módulo serve como ponto de partida para definir nossas próprias classes abstratas.

from abc import ABC

class MinhaClasseAbstrata(ABC):
    pass

# Esta classe não possui métodos abstratos, mas ainda assim é considerada abstrata

Embora a classe acima não tenha métodos abstratos, herdar de ABC já a torna uma classe abstrata conceitualmente. No entanto, o verdadeiro poder das ABCs vem com o uso de métodos abstratos.

3. Métodos Abstratos com @abstractmethod

O decorador @abstractmethod é a ferramenta principal para definir métodos que devem ser obrigatoriamente implementados pelas subclasses. Qualquer classe que herde de uma ABC e não implemente todos os seus métodos abstratos não poderá ser instanciada.

from abc import ABC, abstractmethod

class Forma(ABC):
    @abstractmethod
    def area(self):
        pass

class Quadrado(Forma):
    def __init__(self, lado):
        self.lado = lado

    def area(self):
        return self.lado ** 2

class Circulo(Forma):
    def __init__(self, raio):
        self.raio = raio

    def area(self):
        return 3.14159 * self.raio ** 2

# Uso correto
quadrado = Quadrado(5)
print(quadrado.area())  # 25

# Erro: não é possível instanciar classe abstrata
# forma = Forma()  # TypeError: Can't instantiate abstract class Forma with abstract methods area

Se uma subclasse não implementar todos os métodos abstratos, o Python levantará um TypeError ao tentar instanciá-la:

class TrianguloIncompleto(Forma):
    pass

# triangulo = TrianguloIncompleto()  # TypeError

4. Propriedades e Métodos de Classe Abstratos

O módulo abc também permite definir propriedades e métodos estáticos/de classe como abstratos:

from abc import ABC, abstractmethod

class Animal(ABC):
    @property
    @abstractmethod
    def som(self):
        pass

    @classmethod
    @abstractmethod
    def mover(cls):
        pass

    @staticmethod
    @abstractmethod
    def respirar():
        pass

class Cachorro(Animal):
    @property
    def som(self):
        return "Au au"

    @classmethod
    def mover(cls):
        return "Correndo"

    @staticmethod
    def respirar():
        return "Inspirando e expirando"

cachorro = Cachorro()
print(cachorro.som)      # Au au
print(Cachorro.mover())  # Correndo
print(Cachorro.respirar())  # Inspirando e expirando

Note que a ordem dos decoradores é importante: @property deve vir antes de @abstractmethod.

5. Métodos Concretos em Classes Abstratas

Classes abstratas não precisam conter apenas métodos abstratos. Elas podem incluir métodos concretos com implementação completa, que as subclasses podem herdar ou sobrescrever:

from abc import ABC, abstractmethod

class Veiculo(ABC):
    def __init__(self, nome):
        self.nome = nome
        self.ligado = False

    def ligar(self):
        self.ligado = True
        return f"{self.nome} ligado"

    def desligar(self):
        self.ligado = False
        return f"{self.nome} desligado"

    @abstractmethod
    def acelerar(self):
        pass

class Carro(Veiculo):
    def acelerar(self):
        if self.ligado:
            return f"{self.nome} acelerando"
        return f"Ligue {self.nome} primeiro"

carro = Carro("Fusca")
print(carro.ligar())      # Fusca ligado
print(carro.acelerar())   # Fusca acelerando

O método super() pode ser usado para chamar implementações da classe pai:

class CarroEletrico(Carro):
    def ligar(self):
        return super().ligar() + " silenciosamente"

eletrico = CarroEletrico("Tesla")
print(eletrico.ligar())  # Tesla ligado silenciosamente

6. Herança Múltipla com ABCs

Python suporta herança múltipla com classes abstratas, o que permite combinar funcionalidades de diferentes ABCs:

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def fazer_som(self):
        pass

class Voador(ABC):
    @abstractmethod
    def voar(self):
        pass

class Pato(Animal, Voador):
    def fazer_som(self):
        return "Quack!"

    def voar(self):
        return "Voando como um pato"

pato = Pato()
print(pato.fazer_som())  # Quack!
print(pato.voar())       # Voando como um pato

A Method Resolution Order (MRO) determina a ordem de busca de métodos em herança múltipla. Você pode verificá-la com ClassName.__mro__.

7. Registro de Subclasses Virtuais com register()

O método register() permite declarar que uma classe é subclasse de uma ABC sem que ela herde diretamente dela. Isso é útil para integração com classes de bibliotecas externas:

from abc import ABC, abstractmethod

class Iteravel(ABC):
    @abstractmethod
    def iterar(self):
        pass

# Registrando list como subclasse virtual
Iteravel.register(list)

# Agora list é considerada subclasse de Iteravel
print(issubclass(list, Iteravel))      # True
print(isinstance([1, 2, 3], Iteravel)) # True

# Mas list não herda de Iteravel
print(list.__bases__)  # (<class 'object'>,)

Isso permite que você use isinstance() e issubclass() para verificar conformidade com uma interface, mesmo que a classe não tenha sido projetada para isso.

8. Boas Práticas e Casos de Uso

ABCs são mais adequadas quando você precisa garantir que subclasses implementem uma interface específica. Em Python, no entanto, duck typing e protocolos (PEP 544) oferecem alternativas mais flexíveis.

Quando usar ABCs:
- Em bibliotecas e frameworks onde a consistência da interface é crítica
- Quando você precisa de herança múltipla com comportamento compartilhado
- Para criar hierarquias de classes com validação em tempo de instanciação

Quando evitar:
- Em código pequeno ou scripts simples
- Quando duck typing é suficiente
- Se você precisa de máxima flexibilidade (prefira protocolos)

O módulo collections.abc fornece diversas ABCs úteis como Sequence, Mapping, Iterable, que você pode usar para verificar interfaces de coleções. O módulo numbers também oferece ABCs para tipos numéricos.

Lembre-se: mantenha suas ABCs focadas e coesas. Uma classe abstrata deve representar um conceito claro e bem definido, com o mínimo de métodos abstratos necessários para estabelecer o contrato.

Referências