Nesta aula, você vai aprender a usar o módulo abc para criar classes abstratas em Python. Classes abstratas são fundamentais para o design orientado a objetos, pois permitem definir uma interface comum e garantir que subclasses implementem certos métodos. Isso torna o código mais robusto e expressivo, especialmente em projetos grandes.

Além disso, vamos explorar o conceito de interfaces em Python, que não possui uma palavra-chave própria como em Java, mas pode ser implementada via classes abstratas. Também daremos uma olhada nos Protocolos (PEP 544), que oferecem uma alternativa para verificação estrutural de tipos. Ao final, você terá uma compreensão sólida de como e quando usar cada abordagem.

O módulo abc

O módulo abc (Abstract Base Classes) fornece as ferramentas para definir classes abstratas em Python. A classe ABC é a base para criar classes abstratas, e o decorador @abstractmethod marca métodos que devem ser implementados pelas subclasses.

Uma classe abstrata não pode ser instanciada diretamente; ela serve como um contrato para as subclasses. Se uma subclasse não implementar todos os métodos abstratos, ela também se torna abstrata e não pode ser instanciada. Isso garante que todos os objetos concretos tenham a funcionalidade esperada.

from abc import ABC, abstractmethod

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

    @abstractmethod
    def mover(self):
        pass

# Tentativa de instanciar uma classe abstrata gera erro:
# animal = Animal()  # TypeError: Can't instantiate abstract class Animal with abstract methods mover, fazer_som

class Cachorro(Animal):
    def fazer_som(self):
        return "Au au!"

    def mover(self):
        return "Correndo"

c = Cachorro()
print(c.fazer_som())  # Au au!

O módulo abc também oferece outros utilitários, como @abstractproperty (depreciado em favor de @property combinado com @abstractmethod) e ABCMeta, que é a metaclasse usada por ABC. Você raramente precisará usar ABCMeta diretamente, mas é útil saber que ele controla a verificação de métodos abstratos.

Métodos abstratos

Um método abstrato é um método que tem declaração, mas não tem implementação (ou tem uma implementação que deve ser sobrescrita). Em Python, usamos o decorador @abstractmethod para marcá-lo. A implementação real fica nas subclasses.

Além de métodos comuns, você pode definir propriedades abstratas e métodos de classe abstratos. Isso permite criar contratos mais ricos. Por exemplo, uma propriedade abstrata força que toda subclasse defina um atributo com aquele nome.

from abc import ABC, abstractmethod

class Forma(ABC):
    @property
    @abstractmethod
    def area(self):
        """Retorna a área da forma."""
        pass

    @classmethod
    @abstractmethod
    def criar_aleatorio(cls):
        """Cria uma instância com parâmetros aleatórios."""
        pass

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

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

    @classmethod
    def criar_aleatorio(cls):
        import random
        return cls(random.random() * 10)

c = Circulo.criar_aleatorio()
print(c.area)  # Valor da área

No exemplo acima, area é uma propriedade abstrata e criar_aleatorio é um método de classe abstrato. Subclasses precisam implementar ambos. Isso é útil para garantir uma API consistente.

Interfaces em Python

Python não tem uma palavra-chave interface como Java ou C#. Em vez disso, classes abstratas podem ser usadas para simular interfaces, especialmente quando você quer definir apenas métodos abstratos, sem implementação concreta. Nesse caso, a classe abstrata atua como uma interface pura.

No entanto, é importante notar que Python adota o duck typing: se um objeto tem os métodos esperados, ele pode ser usado, independentemente de sua herança. Portanto, as interfaces em Python são mais sobre documentação e verificação explícita do que sobre imposição rígida.

from abc import ABC, abstractmethod

class InterfaceRepositorio(ABC):
    @abstractmethod
    def salvar(self, entidade):
        """Salva a entidade."""
        pass

    @abstractmethod
    def buscar(self, id):
        """Busca uma entidade pelo id."""
        pass

class RepositorioUsuario(InterfaceRepositorio):
    def salvar(self, usuario):
        print(f"Salvando usuário {usuario.nome} no banco.")

    def buscar(self, id):
        print(f"Buscando usuário com id {id}")
        return {"id": id, "nome": "João"}

# Uso
repo = RepositorioUsuario()
repo.salvar(usuario)  # Supondo que usuario exista

Nesse exemplo, InterfaceRepositorio define o contrato que qualquer repositório deve seguir. Subclasses concretas implementam os métodos. Essa abordagem facilita a troca de implementações e testes com mocks.

Protocolos (visão geral)

Protocolos, introduzidos no Python 3.8 (PEP 544), oferecem uma alternativa para verificação estrutural de tipos. Enquanto classes abstratas exigem herança explícita, os protocolos permitem que você defina uma interface que qualquer classe pode satisfazer, mesmo sem herdar dela. Isso é útil para tipagem estática e duck typing.

Um protocolo é definido com a classe typing.Protocol e usa métodos normais (sem @abstractmethod). A verificação é feita em tempo de análise estática (por exemplo, com mypy) e, opcionalmente, em tempo de execução com @runtime_checkable.

from typing import Protocol

class PodeFalar(Protocol):
    def falar(self) -> str:
        ...

class Pessoa:
    def falar(self) -> str:
        return "Oi!"

class Robo:
    def falar(self) -> str:
        return "Beep boop"

def cumprimentar(entidade: PodeFalar) -> None:
    print(entidade.falar())

cumprimentar(Pessoa())  # Funciona
cumprimentar(Robo())    # Funciona

No exemplo, Pessoa e Robo não herdam de PodeFalar, mas possuem o método falar com a mesma assinatura, então são estruturalmente compatíveis. Em tempo de execução, sem @runtime_checkable, a função cumprimentar aceitaria qualquer objeto; a verificação seria apenas estática. Para verificação em tempo de execução, use o decorador:

from typing import Protocol, runtime_checkable

@runtime_checkable
class PodeFalar(Protocol):
    def falar(self) -> str:
        ...

print(isinstance(Pessoa(), PodeFalar))  # True

Protocolos são particularmente úteis para bibliotecas e APIs, onde você quer aceitar qualquer objeto que implemente os métodos necessários, sem forçar herança. Eles complementam as classes abstratas: use classes abstratas quando você precisa de herança e reuso de código, e protocolos quando você quer apenas definir uma interface estrutural.

Boas práticas

  • Use classes abstratas para hierarquias que compartilham implementação e estado. Não as use apenas para impor métodos, se você não precisa de herança.
  • Prefira protocolos para contratos estruturais, especialmente em funções que aceitam múltiplos tipos.
  • Documente o contrato com docstrings nos métodos abstratos.
  • Evite classes abstratas com muitos métodos; isso pode indicar que a abstração não está bem definida.
  • Use @abstractmethod em combinação com @property ou @classmethod para forçar implementação de características especiais.

Referências

Exercícios

  1. Crie uma classe abstrata Veiculo com métodos abstratos acelerar() e frear(). Implemente duas subclasses: Carro e Bicicleta. No Carro, acelerar() imprime "Acelerando carro" e frear() imprime "Freando carro". Na Bicicleta, acelerar() imprime "Pedalando" e frear() imprime "Apertando freio". Teste instanciando cada um.

    ✓ Resposta:
    from abc import ABC, abstractmethod
    
    class Veiculo(ABC):
        @abstractmethod
        def acelerar(self):
            pass
    
        @abstractmethod
        def frear(self):
            pass
    
    class Carro(Veiculo):
        def acelerar(self):
            print("Acelerando carro")
    
        def frear(self):
            print("Freando carro")
    
    class Bicicleta(Veiculo):
        def acelerar(self):
            print("Pedalando")
    
        def frear(self):
            print("Apertando freio")
    
    # Teste
    carro = Carro()
    carro.acelerar()
    carro.frear()
    
    bike = Bicicleta()
    bike.acelerar()
    bike.frear()
    
  2. Defina uma interface FormaGeometrica (classe abstrata) com métodos area() e perimetro(). Implemente Quadrado e Circulo. Crie uma função exibir_dados(forma) que recebe um objeto com essa interface e imprime a área e o perímetro. Teste com ambas as classes.

    ✓ Resposta:
    from abc import ABC, abstractmethod
    import math
    
    class FormaGeometrica(ABC):
        @abstractmethod
        def area(self):
            pass
    
        @abstractmethod
        def perimetro(self):
            pass
    
    class Quadrado(FormaGeometrica):
        def __init__(self, lado):
            self.lado = lado
    
        def area(self):
            return self.lado ** 2
    
        def perimetro(self):
            return 4 * self.lado
    
    class Circulo(FormaGeometrica):
        def __init__(self, raio):
            self.raio = raio
    
        def area(self):
            return math.pi * self.raio ** 2
    
        def perimetro(self):
            return 2 * math.pi * self.raio
    
    def exibir_dados(forma):
        print(f"Área: {forma.area():.2f}")
        print(f"Perímetro: {forma.perimetro():.2f}")
    
    # Teste
    q = Quadrado(5)
    c = Circulo(3)
    exibir_dados(q)
    exibir_dados(c)
    
  3. Explique a diferença entre classes abstratas e protocolos em Python. Dê um exemplo onde cada um seria mais adequado.

    ✓ Resposta: Classes abstratas são usadas quando você quer definir uma hierarquia de classes com implementação compartilhada e forçar subclasses a implementar métodos. Elas exigem herança explícita. Protocolos são usados para definir uma interface estrutural: qualquer classe que tenha os métodos necessários pode ser considerada como implementando o protocolo, sem precisar herdar. Classes abstratas são mais adequadas quando há código comum para reutilizar (ex: método que já tem implementação base). Protocolos são mais adequados para bibliotecas e funções que aceitam qualquer objeto com determinados métodos, como em duck typing. Exemplo: Animal com fazer_som pode ser uma classe abstrata para Cachorro e Gato; um protocolo Voador seria adequado para aceitar qualquer objeto com método voar(), como Passaro e Aviao.
  4. Crie um protocolo chamado Imprimivel com um método imprimir() que recebe uma string e a imprime. Implemente duas classes que satisfaçam esse protocolo sem herdar dele: Console e Arquivo. Use @runtime_checkable e verifique com isinstance que objetos dessas classes são instâncias do protocolo.

    ✓ Resposta:
    from typing import Protocol, runtime_checkable
    
    @runtime_checkable
    class Imprimivel(Protocol):
        def imprimir(self, texto: str) -> None:
            ...
    
    class Console:
        def imprimir(self, texto: str) -> None:
            print(texto)
    
    class Arquivo:
        def __init__(self, nome_arquivo):
            self.nome_arquivo = nome_arquivo
    
        def imprimir(self, texto: str) -> None:
            with open(self.nome_arquivo, 'w') as f:
                f.write(texto)
    
    # Teste
    c = Console()
    a = Arquivo("saida.txt")
    print(isinstance(c, Imprimivel))  # True
    print(isinstance(a, Imprimivel))  # True
    
  5. Usando ABC, crie uma classe abstrata Funcionario com um atributo nome, um método abstrato calcular_salario() e um método concreto exibir_informacoes() que imprime o nome e o salário. Implemente FuncionarioHorista (salário por horas) e FuncionarioAssalariado (salário fixo). Instancie e chame exibir_informacoes() para cada.

    ✓ Resposta:
    from abc import ABC, abstractmethod
    
    class Funcionario(ABC):
        def __init__(self, nome):
            self.nome = nome
    
        @abstractmethod
        def calcular_salario(self):
            pass
    
        def exibir_informacoes(self):
            print(f"Nome: {self.nome}, Salário: {self.calcular_salario():.2f}")
    
    class FuncionarioHorista(Funcionario):
        def __init__(self, nome, horas_trabalhadas, valor_hora):
            super().__init__(nome)
            self.horas_trabalhadas = horas_trabalhadas
            self.valor_hora = valor_hora
    
        def calcular_salario(self):
            return self.horas_trabalhadas * self.valor_hora
    
    class FuncionarioAssalariado(Funcionario):
        def __init__(self, nome, salario_mensal):
            super().__init__(nome)
            self.salario_mensal = salario_mensal
    
        def calcular_salario(self):
            return self.salario_mensal
    
    # Teste
    f1 = FuncionarioHorista("Ana", 160, 50)
    f2 = FuncionarioAssalariado("Bruno", 8000)
    f1.exibir_informacoes()
    f2.exibir_informacoes()
    

Observações finais

Classes abstratas e protocolos são ferramentas poderosas para criar código flexível e bem estruturado. Lembre-se de que Python valoriza a simplicidade e o duck typing; use essas ferramentas quando elas agregarem clareza e segurança ao seu projeto, sem exagerar na complexidade. Pratique os exercícios e experimente combinar classes abstratas com protocolos para entender melhor as diferenças e quando cada uma é mais apropriada.