Classes abstratas são um conceito fundamental da programação orientada a objetos que permite definir uma interface comum para um grupo de subclasses, garantindo que certos métodos sejam implementados. Em Python, isso é feito através do módulo abc (Abstract Base Classes). Nesta aula, vamos explorar como criar classes abstratas, declarar métodos abstratos, simular interfaces e conhecer os protocolos, uma alternativa moderna introduzida no Python 3.8.

Entender classes abstratas é essencial para projetar hierarquias de classes robustas, promovendo o reuso de código e a consistência. Diferente de linguagens como Java, Python não possui uma palavra-chave interface, mas o módulo abc oferece mecanismos equivalentes.

abc

O módulo abc fornece a metaclasse ABCMeta e o decorador abstractmethod para definir classes abstratas. Uma classe que herda de ABC (que usa ABCMeta) não pode ser instanciada diretamente se possuir métodos abstratos não implementados.

Exemplo básico de declaração de uma classe abstrata:

from abc import ABC, abstractmethod

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

# Tentar instanciar Animal gera TypeError
# animal = Animal()  # TypeError: Can't instantiate abstract class Animal with abstract methods fazer_som

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

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

Observe que a classe Animal não pode ser instanciada porque possui um método abstrato. A subclasse Cachorro deve implementar fazer_som para ser concreta. Se não implementar, também será abstrata e não poderá ser instanciada.

Métodos abstratos

Métodos abstratos são declarados com o decorador @abstractmethod e não possuem implementação na classe base. Eles forçam as subclasses a fornecer uma implementação concreta. É possível ter métodos concretos (com implementação) em classes abstratas, que podem ser herdados ou sobrescritos.

Exemplo com método concreto:

from abc import ABC, abstractmethod

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

    def descricao(self):
        return f"Sou uma forma com área {self.area()}"

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

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

q = Quadrado(4)
print(q.descricao())  # Sou uma forma com área 16

Note que descricao é um método concreto que chama o método abstrato area. Isso é permitido e muito útil. Além disso, você pode usar @abstractmethod em métodos estáticos, de classe e propriedades.

Interfaces em Python

Python não tem uma palavra-chave interface, mas podemos simular interfaces usando classes abstratas com todos os métodos abstratos (sem implementação). Uma interface define um contrato que as classes devem seguir. Diferente de herança múltipla, interfaces focam apenas no comportamento.

Exemplo de interface:

from abc import ABC, abstractmethod

class Imprimivel(ABC):
    @abstractmethod
    def imprimir(self):
        pass

class Scanner(ABC):
    @abstractmethod
    def digitalizar(self):
        pass

class Multifuncional(Imprimivel, Scanner):
    def imprimir(self):
        print("Imprimindo...")

    def digitalizar(self):
        print("Digitalizando...")

m = Multifuncional()
m.imprimir()
m.digitalizar()

Note que a classe Multifuncional implementa duas interfaces. Isso mostra como Python lida com herança múltipla de classes abstratas. É importante que as interfaces tenham apenas métodos abstratos, sem estado, para evitar problemas com herança múltipla (diamante).

Protocolos (visão geral)

Protocolos foram introduzidos no Python 3.8 via PEP 544 como uma forma de duck typing estático. Eles permitem definir uma interface sem exigir herança, usando typing.Protocol. Uma classe que implementa os métodos definidos no protocolo é considerada compatível, mesmo sem herdar dele.

Exemplo de protocolo:

from typing import Protocol

class Voador(Protocol):
    def voar(self) -> str:
        ...

class Passaro:
    def voar(self) -> str:
        return "Batendo asas"

class Aviao:
    def voar(self) -> str:
        return "Usando turbinas"

def fazer_voar(objeto: Voador):
    print(objeto.voar())

fazer_voar(Passaro())  # Batendo asas
fazer_voar(Aviao())    # Usando turbinas

Protocolos são úteis para tipagem estática e duck typing, sem acoplamento de herança. Diferente de ABCs, não há verificação em tempo de execução (a menos que use isinstance com runtime_checkable). Protocolos são mais leves e preferíveis quando você só precisa de uma interface sem hierarquia.

Boas práticas

Ao usar classes abstratas, prefira herança simples para evitar complexidade. Use interfaces (classes com todos métodos abstratos) para definir contratos. Para duck typing, considere usar protocolos. Sempre documente os métodos abstratos com docstrings claras. Evite criar classes abstratas com muitos métodos; prefira interfaces pequenas e focadas (princípio da segregação de interfaces).

Referências

Exercícios

  1. Crie uma classe abstrata chamada Veiculo com um método abstrato mover e um método concreto parar que imprime "Veículo parado". Depois implemente subclasses Carro e Bicicleta.
  2. ✓ Resposta:
    from abc import ABC, abstractmethod
    
    class Veiculo(ABC):
        @abstractmethod
        def mover(self):
            pass
    
        def parar(self):
            print("Veículo parado")
    
    class Carro(Veiculo):
        def mover(self):
            print("Carro se movendo")
    
    class Bicicleta(Veiculo):
        def mover(self):
            print("Bicicleta se movendo")
    
    c = Carro()
    c.mover()
    c.parar()
    b = Bicicleta()
    b.mover()
    b.parar()
  3. Defina uma interface Serializavel com métodos abstratos serializar e desserializar. Implemente em uma classe Pessoa que serializa para JSON.
  4. ✓ Resposta:
    from abc import ABC, abstractmethod
    import json
    
    class Serializavel(ABC):
        @abstractmethod
        def serializar(self):
            pass
    
        @abstractmethod
        def desserializar(self, dados):
            pass
    
    class Pessoa(Serializavel):
        def __init__(self, nome, idade):
            self.nome = nome
            self.idade = idade
    
        def serializar(self):
            return json.dumps({"nome": self.nome, "idade": self.idade})
    
        def desserializar(self, dados):
            obj = json.loads(dados)
            self.nome = obj["nome"]
            self.idade = obj["idade"]
    
    p = Pessoa("João", 30)
    dados = p.serializar()
    print(dados)
    p2 = Pessoa("", 0)
    p2.desserializar(dados)
    print(p2.nome, p2.idade)
  5. Crie um protocolo Nadador com um método nadar que retorna uma string. Implemente em classes Peixe e Humano sem herdar do protocolo. Escreva uma função que aceite Nadador e chame nadar.
  6. ✓ Resposta:
    from typing import Protocol
    
    class Nadador(Protocol):
        def nadar(self) -> str:
            ...
    
    class Peixe:
        def nadar(self) -> str:
            return "Nadando com barbatanas"
    
    class Humano:
        def nadar(self) -> str:
            return "Nadando com braçadas"
    
    def fazer_nadar(nadador: Nadador):
        print(nadador.nadar())
    
    fazer_nadar(Peixe())
    fazer_nadar(Humano())
  7. Explique a diferença entre uma classe abstrata com métodos concretos e um protocolo. Dê um exemplo onde cada um é mais adequado.
  8. ✓ Resposta:

    Classes abstratas podem ter implementação (métodos concretos) e estado (atributos), enquanto protocolos são puramente estruturais (só definem métodos, sem implementação). Classes abstratas são adequadas quando há uma hierarquia com código compartilhado (ex: classe base Animal com método concreto respirar). Protocolos são melhores para duck typing estático, quando não se quer acoplamento de herança (ex: interface Voador que pode ser implementada por classes não relacionadas).

  9. Crie uma classe abstrata FormaGeometrica com um método abstrato calcular_area e um método concreto exibir_area que imprime a área. Implemente Circulo e Retangulo. Depois, crie uma lista de formas e exiba a área de cada uma.
  10. ✓ Resposta:
    from abc import ABC, abstractmethod
    import math
    
    class FormaGeometrica(ABC):
        @abstractmethod
        def calcular_area(self):
            pass
    
        def exibir_area(self):
            print(f"Área: {self.calcular_area()}")
    
    class Circulo(FormaGeometrica):
        def __init__(self, raio):
            self.raio = raio
    
        def calcular_area(self):
            return math.pi * self.raio ** 2
    
    class Retangulo(FormaGeometrica):
        def __init__(self, largura, altura):
            self.largura = largura
            self.altura = altura
    
        def calcular_area(self):
            return self.largura * self.altura
    
    formas = [Circulo(3), Retangulo(4, 5)]
    for forma in formas:
        forma.exibir_area()