Abstração e classes abstratas no Python: abc.ABC e @abstractmethod

Cansado de programar?

Conheça a melhor e mais completa formação de Python e Django e sinta-se um programador verdadeiramente competente. Além de Python e Django, você também vai aprender Banco de Dados, SQL, HTML, CSS, Javascript, Bootstrap e muito mais!

Quero aprender Python e Django de Verdade! Quero aprender!
Suporte

Tire suas dúvidas diretamente com o professor

Projetos práticos

Projetos práticos voltados para o mercado de trabalho

Prática profissional

Formação moderna com foco na prática profissional

Resposta rápida

Abstração é expor só o essencial de um objeto e esconder os detalhes. Em Python, uma classe abstrata herda de abc.ABC e marca métodos com @abstractmethod: ela não pode ser instanciada, e toda subclasse é obrigada a implementar esses métodos, senão o Python lança TypeError.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from abc import ABC, abstractmethod

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

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

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

print(Quadrado(3).area())  # 9
# Forma() -> TypeError: Can't instantiate abstract class Forma ...

Resumo em 30 segundos:

  • Abstração = modelar o “o que faz” e esconder o “como faz”.
  • class X(ABC) + @abstractmethod = classe abstrata.
  • Instanciar a classe abstrata, ou uma filha que não implementou todos os métodos abstratos, gera TypeError.
  • Classe abstrata pode ter __init__ e métodos concretos; método abstrato pode ter código (chamado com super()).
  • Python não tem interface: usa ABC (com herança) ou typing.Protocol (sem herança).

Salve salve Pythonista!

Este post fecha a nossa série sobre Programação Orientada a Objetos. A série começa na introdução à POO no Python, passa por classes e objetos, herança e polimorfismo. Como classes abstratas dependem de herança, vale ter esse post fresco na memória.

Aqui você vai ver o que é abstração, como criar classes abstratas com abc.ABC e @abstractmethod, o TypeError que aparece ao instanciá-las (e por que a mensagem muda conforme a versão do Python), propriedades abstratas e a eterna dúvida “interface ou classe abstrata?”. No final, erros comuns e exercícios resolvidos. Os exemplos foram executados no Python 3.10, e as mensagens de erro que mudam de versão foram conferidas também no 3.11, 3.12, 3.13 e 3.14.

Então… Bora pro post! :rocket:

Vá Direto ao Assunto…

O que é abstração na programação orientada a objetos

O que é abstração em POO? Abstração é o princípio de representar um objeto apenas pelas características e comportamentos essenciais para quem vai usá-lo, escondendo os detalhes de como ele funciona por dentro. Em código, isso significa oferecer uma interface simples (por exemplo, um método enviar(texto)) e deixar a complexidade dentro da classe. Em Python, classes abstratas tornam essa interface explícita e obrigatória.

Você usa abstrações o tempo todo. Ao chamar print(), você não pensa em como o texto chega ao terminal. Ao chamar lista.sort(), não precisa saber qual algoritmo de ordenação roda por baixo. Na introdução à POO usamos o exemplo do tocador de DVD: você aperta “play” sem saber nada sobre o laser.

No seu próprio código, a abstração aparece quando uma função depende do que um objeto faz, não de como ele faz:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def enviar_relatorio(canal, texto):
    canal.enviar(texto)  # não importa COMO o canal envia


class Email:
    def enviar(self, texto):
        print(f"[email] conectando ao servidor SMTP... {texto}")


class Telegram:
    def enviar(self, texto):
        print(f"[telegram] chamando a API do bot... {texto}")


enviar_relatorio(Email(), "vendas de setembro")
enviar_relatorio(Telegram(), "vendas de setembro")
1
2
[email] conectando ao servidor SMTP... vendas de setembro
[telegram] chamando a API do bot... vendas de setembro

A função enviar_relatorio() conhece só a abstração “algo que sabe enviar(texto)”. Os detalhes (SMTP, API do bot) ficam escondidos em cada classe.

Esse código funciona, mas tem um ponto fraco: nada garante que um novo canal vai ter o método enviar(). Se alguém criar class Whatsapp com um método mandar(), o erro só aparece em produção. As classes abstratas resolvem isso transformando a abstração em um contrato que o Python verifica.

Não confunda abstração com encapsulamento. Abstração decide o que expor (a interface). Encapsulamento decide como proteger os dados internos, em Python com _atributo e com @property.

Classes abstratas com abc.ABC e @abstractmethod

O módulo abc (de Abstract Base Classes) da biblioteca padrão, criado pela PEP 3119, traz duas peças:

  • ABC: a classe da qual a sua classe abstrata herda.
  • @abstractmethod: o decorador que marca um método como obrigatório para as filhas.

Veja um sistema de pagamentos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from abc import ABC, abstractmethod


class Pagamento(ABC):
    def __init__(self, valor):
        self.valor = valor

    @abstractmethod
    def processar(self):
        """Cada meio de pagamento processa do seu jeito."""

    def recibo(self):  # método concreto, herdado pronto
        return f"{type(self).__name__}: {self.processar()}"


class Pix(Pagamento):
    def processar(self):
        return f"R$ {self.valor:.2f} transferidos na hora"


class Cartao(Pagamento):
    def __init__(self, valor, parcelas=1):
        super().__init__(valor)
        self.parcelas = parcelas

    def processar(self):
        parcela = self.valor / self.parcelas
        return f"{self.parcelas}x de R$ {parcela:.2f}"


for pagamento in [Pix(150), Cartao(300, parcelas=3)]:
    print(pagamento.recibo())
1
2
Pix: R$ 150.00 transferidos na hora
Cartao: 3x de R$ 100.00

Repare no que a classe abstrata Pagamento tem:

  • Um __init__ normal, que as filhas herdam (o Cartao estende com super().__init__(valor), como vimos no post de herança).
  • Um método abstrato, processar(), cujo corpo é só uma docstring. Também é comum usar ... ou pass.
  • Um método concreto, recibo(), que chama o método abstrato. Ele funciona em qualquer filha porque todas são obrigadas a ter processar(). Isso é polimorfismo garantido por contrato.

Em código mais antigo você pode encontrar class Pagamento(metaclass=ABCMeta):. É equivalente: ABC é só um atalho para uma classe cuja metaclasse é ABCMeta, e herdar dela é a forma recomendada hoje.

TypeError ao instanciar uma classe abstrata

A regra principal das classes abstratas: não é possível criar objetos delas, nem de filhas que deixaram algum método abstrato sem implementação. O Python verifica isso no momento da criação do objeto.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from abc import ABC, abstractmethod


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

    @abstractmethod
    def perimetro(self):
        ...


Forma()

No Python 3.10 (o python3 local usado neste post), a última linha do traceback é:

1
TypeError: Can't instantiate abstract class Forma with abstract methods area, perimetro

Atenção: a mensagem varia conforme a versão. A partir do Python 3.12, o mesmo código mostra:

1
TypeError: Can't instantiate abstract class Forma without an implementation for abstract methods 'area', 'perimetro'

Conferimos as duas formas: 3.10 e 3.11 usam a primeira; 3.12, 3.13 e 3.14 usam a segunda. O tipo da exceção (TypeError) e o momento em que ela acontece são os mesmos em todas. Por isso, se você for testar esse comportamento (em um teste automatizado, por exemplo), verifique o tipo TypeError, nunca o texto exato da mensagem.

O mesmo erro aparece com uma filha incompleta:

1
2
3
4
5
6
7
8
9
10
class Quadrado(Forma):
    def __init__(self, lado):
        self.lado = lado

    def area(self):
        return self.lado ** 2
    # falta perimetro()


Quadrado(2)
1
TypeError: Can't instantiate abstract class Quadrado with abstract method perimetro

(No 3.12+: ... without an implementation for abstract method 'perimetro'.) A mensagem lista exatamente o que falta, o que ajuda muito. E compare com o raise NotImplementedError que vimos no post de polimorfismo: lá o erro só aparece quando alguém chama o método; aqui ele aparece ao criar o objeto, bem mais cedo.

Por baixo dos panos, o Python guarda os nomes pendentes em __abstractmethods__, e o módulo inspect tem uma função pronta para perguntar se a classe ainda é abstrata:

1
2
3
4
5
import inspect

print(sorted(Forma.__abstractmethods__))     # ['area', 'perimetro']
print(sorted(Quadrado.__abstractmethods__))  # ['perimetro']
print(inspect.isabstract(Quadrado))          # True

Está curtindo esse conteúdo? :thumbsup:

Que tal receber 30 dias de conteúdo direto na sua Caixa de Entrada?

Sua assinatura não pôde ser validada.
Você fez sua assinatura com sucesso.

Assine as PyDicas e receba 30 dias do melhor conteúdo Python na sua Caixa de Entrada: direto e sem enrolação!

Métodos abstratos com implementação e propriedades abstratas

Duas coisas que surpreendem quem vem de Java.

1. Método abstrato pode ter código. O @abstractmethod obriga a filha a sobrescrever o método, mas não impede que a mãe tenha uma implementação. A filha pode reaproveitá-la com super():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from abc import ABC, abstractmethod


class Notificador(ABC):
    @abstractmethod
    def enviar(self, mensagem):
        # implementação padrão que as filhas podem reaproveitar
        return mensagem.strip().capitalize()


class Sms(Notificador):
    def enviar(self, mensagem):
        texto = super().enviar(mensagem)
        return f"SMS: {texto}"


print(Sms().enviar("   seu código é 4321, não compartilhe   "))
1
SMS: Seu código é 4321, não compartilhe

2. Propriedades também podem ser abstratas. Empilhe @property e @abstractmethod, nessa ordem (@property por fora, em cima). A ordem invertida quebra, e é o erro 3 lá embaixo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from abc import ABC, abstractmethod


class Funcionario(ABC):
    def __init__(self, nome):
        self.nome = nome

    @property
    @abstractmethod
    def salario(self):
        ...


class Estagiario(Funcionario):
    @property
    def salario(self):
        return 1500


class Clt(Funcionario):
    def __init__(self, nome, base):
        super().__init__(nome)
        self.base = base

    @property
    def salario(self):
        return self.base * 1.2


for f in [Estagiario("Leo"), Clt("Ana", 5000)]:
    print(f.nome, f.salario)
1
2
Leo 1500
Ana 6000.0

Cada filha calcula o salário do seu jeito, e quem usa acessa f.salario como um atributo comum. Se @property ainda é novidade, veja como utilizar @property no Python. O mesmo empilhamento funciona com @classmethod e @staticmethod, sempre com o @abstractmethod como decorador mais interno.

Interface vs classe abstrata em Python

Em Java e C#, interface é uma construção da linguagem: só declara métodos, sem código, e uma classe pode implementar várias. Classe abstrata pode ter código e estado, mas só se herda de uma. Python não tem a palavra-chave interface, então a distinção vira uma questão de como você escreve a classe:

  • Classe abstrata “interface”: uma ABC que só tem métodos abstratos, sem __init__ nem código. Como Python tem herança múltipla, uma classe pode “implementar” várias delas.
  • Classe abstrata “de verdade”: uma ABC com estado e métodos concretos, como o Pagamento acima.
  • Protocolo: um typing.Protocol, que descreve os métodos exigidos sem herança (o duck typing escrito de forma explícita), apresentado no post de polimorfismo.

Uma ABC no papel de interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from abc import ABC, abstractmethod


class Repositorio(ABC):  # só métodos abstratos: uma "interface"
    @abstractmethod
    def salvar(self, item): ...

    @abstractmethod
    def buscar(self, id_): ...


class RepositorioMemoria(Repositorio):
    def __init__(self):
        self._dados = {}

    def salvar(self, item):
        self._dados[item["id"]] = item

    def buscar(self, id_):
        return self._dados.get(id_)


repo = RepositorioMemoria()
repo.salvar({"id": 1, "nome": "Ana"})
print(repo.buscar(1))                  # {'id': 1, 'nome': 'Ana'}
print(isinstance(repo, Repositorio))   # True

Amanhã você cria um RepositorioPostgres com os mesmos dois métodos, e o resto do sistema não percebe a troca. Esse tipo de contrato é a base de vários padrões de projeto, como o Abstract Factory em Python.

As ABCs prontas de collections.abc

A biblioteca padrão já traz dezenas de classes abstratas no módulo collections.abc, como Sized, Iterable, Sequence e Mapping. Algumas delas reconhecem uma classe só pelos métodos que ela tem, sem exigir herança:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from collections.abc import Sized, Iterable, Sequence


class Playlist:
    def __init__(self, musicas):
        self.musicas = list(musicas)

    def __len__(self):
        return len(self.musicas)


print(isinstance(Playlist([]), Sized))     # True, sem herdar de Sized
print(isinstance(Playlist([]), Iterable))  # False: falta __iter__
print(issubclass(list, Sequence))          # True
print(issubclass(dict, Sequence))          # False

Playlist tem __len__, então conta como Sized, mesmo sem herdar dela. Isso é o duck typing com uma etiqueta oficial. E existe ainda o register(), que declara uma classe como “subclasse virtual” de uma ABC sem herança (por exemplo, Serializavel.register(Pedido) faz isinstance(Pedido(), Serializavel) devolver True). Use com cuidado: o register() não verifica se a classe tem os métodos.

Classe abstrata é ferramenta de quem já pensa em contrato antes de implementação, e isso só vem com prática real de POO. Essa maturidade é construída passo a passo na Jornada Python:

Quando usar classe abstrata

Situação Use Por quê
Várias classes compartilham código e cada uma precisa implementar certos métodos Classe abstrata (abc.ABC) com métodos concretos Reaproveita o código comum e garante o contrato ao instanciar
Você só quer definir um contrato de métodos, e as classes que o seguem são suas ABC só com métodos abstratos Funciona como uma interface e o erro aparece cedo
O contrato precisa aceitar classes de terceiros, que você não pode fazer herdar de nada typing.Protocol Verificação estrutural, sem herança, feita pelo mypy
Sua classe deve se comportar como um tipo nativo (sequência, mapeamento) Herdar de collections.abc Você implementa 2 ou 3 métodos e ganha os outros prontos
Protótipo ou script pequeno, com poucas classes Duck typing puro Menos código; o erro aparece na chamada
Só existe uma implementação e não há previsão de outras Classe concreta comum Abstração sem necessidade é complexidade extra

A última linha é importante: classe abstrata é uma ferramenta para quando existem (ou vão existir) várias implementações de um mesmo conceito. Criar uma ABC para cada classe “por boas práticas” só deixa o código mais longo.

Erros comuns

1. Usar @abstractmethod sem herdar de ABC

1
2
3
4
5
6
7
8
9
10
11
from abc import abstractmethod


class Forma:  # esqueceu de herdar de ABC
    @abstractmethod
    def area(self):
        ...


f = Forma()
print("criou sem erro:", f.area())
1
criou sem erro: None

Não há traceback, e esse é o problema: sem ABC (ou metaclass=ABCMeta), o @abstractmethod não tem efeito, a classe é instanciada normalmente e o método devolve None. Correção: class Forma(ABC):.

2. Instanciar uma filha que não implementou todos os métodos abstratos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def som(self): ...

    @abstractmethod
    def mover(self): ...

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

Cachorro()
1
TypeError: Can't instantiate abstract class Cachorro with abstract method mover

Mensagem do Python 3.10; no 3.12+ ela diz without an implementation for abstract method 'mover'. Correção: implemente mover() em Cachorro. Se a classe for mesmo uma etapa intermediária da hierarquia, tudo bem ela continuar abstrata: só não crie objetos dela.

3. Empilhar @abstractmethod por fora do @property

1
2
3
4
5
6
7
8
from abc import ABC, abstractmethod


class Funcionario(ABC):
    @abstractmethod
    @property
    def salario(self):
        ...
1
AttributeError: attribute '__isabstractmethod__' of 'property' objects is not writable

O erro acontece já na definição da classe. O @abstractmethod precisa ser o decorador mais interno (o mais perto do def). Correção: @property em cima e @abstractmethod embaixo.

4. Erro de digitação no nome do método da filha

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from abc import ABC, abstractmethod


class Pagamento(ABC):
    @abstractmethod
    def processar(self, valor):
        ...


class Boleto(Pagamento):
    def procesar(self, valor):  # erro de digitação: falta um "s"
        return f"boleto de R$ {valor}"


Boleto()
1
TypeError: Can't instantiate abstract class Boleto with abstract method processar

Aqui a classe abstrata ajudou: sem ela, o Boleto seria criado e o erro só apareceria quando alguém chamasse processar(). Correção: renomeie para processar. (No 3.12+ a mensagem termina em without an implementation for abstract method 'processar'.)

5. Achar que o ABC confere os parâmetros do método

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from abc import ABC, abstractmethod


class Pagamento(ABC):
    @abstractmethod
    def processar(self, valor):
        ...


class Pix(Pagamento):
    def processar(self):  # esqueceu o parâmetro valor
        return "pix enviado"


pix = Pix()          # o ABC não reclama
pix.processar(100)   # quebra só aqui
1
TypeError: Pix.processar() takes 1 positional argument but 2 were given

O abc só verifica se existe um método com aquele nome, não se os parâmetros batem. Correção: mantenha a mesma assinatura da mãe (def processar(self, valor)). Para pegar esse tipo de erro antes de rodar, use anotações de tipo e um verificador como o mypy.

Exercícios resolvidos

Tente resolver cada exercício antes de abrir a solução. Todos os códigos foram executados e as saídas conferidas. Para continuar treinando, veja a nossa lista de exercícios de Python.

Exercício 1. Crie a classe abstrata Veiculo com o método abstrato mover(). Implemente Carro e Bicicleta e mostre que Veiculo() não pode ser instanciado.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from abc import ABC, abstractmethod


class Veiculo(ABC):
    @abstractmethod
    def mover(self):
        ...


class Carro(Veiculo):
    def mover(self):
        return "Carro rodando na estrada"


class Bicicleta(Veiculo):
    def mover(self):
        return "Bicicleta pedalando na ciclovia"


for v in [Carro(), Bicicleta()]:
    print(v.mover())

try:
    Veiculo()
except TypeError:
    print("Veiculo é abstrato")

Saída:

1
2
3
Carro rodando na estrada
Bicicleta pedalando na ciclovia
Veiculo é abstrato

O except TypeError captura a exceção pelo tipo, sem depender do texto da mensagem, que muda entre versões do Python.

Exercício 2. (Questão de prova) Sobre classes abstratas em Python, assinale a alternativa correta:

a) Uma classe abstrata não pode ter métodos concretos nem __init__.
b) Basta decorar um método com @abstractmethod, mesmo sem herdar de ABC, para impedir que a classe seja instanciada.
c) Uma subclasse de uma classe abstrata só pode ser instanciada se implementar todos os métodos abstratos herdados.
d) O decorador @abstractmethod verifica se a subclasse implementou o método com os mesmos parâmetros.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from abc import ABC, abstractmethod
import inspect


class Base(ABC):
    @abstractmethod
    def a(self): ...

    @abstractmethod
    def b(self): ...


class Meio(Base):
    def a(self):
        return "a"


class Final(Meio):
    def b(self):
        return "b"


for classe in [Base, Meio, Final]:
    print(classe.__name__, inspect.isabstract(classe))

Saída:

1
2
3
Base True
Meio True
Final False

Resposta: c. Meio implementou só a() e continua abstrata; Final completou o contrato e pode ser instanciada. A a é falsa (o Pagamento deste post tem __init__ e recibo()), a b é falsa (erro 1: sem ABC nada acontece) e a d é falsa (erro 5: só o nome é verificado).

Exercício 3. (Questão de prova) O que acontece ao executar o código abaixo?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def som(self): ...

    @abstractmethod
    def mover(self): ...

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

print("antes")
rex = Cachorro()
print(rex.som())

a) Imprime antes e Au au.
b) Imprime antes e depois lança TypeError na linha rex = Cachorro().
c) Lança TypeError na definição da classe Cachorro, antes de imprimir qualquer coisa.
d) Imprime antes e depois lança NotImplementedError ao chamar rex.som().

Ver solução

Saída: antes, seguida de TypeError: Can't instantiate abstract class Cachorro with abstract method mover (texto do Python 3.10).

Resposta: b. Definir uma subclasse incompleta é permitido (ela só continua abstrata); o erro acontece ao instanciar. Por isso o primeiro print roda. A d descreve o comportamento do raise NotImplementedError, não do abc.

Exercício 4. Crie a classe abstrata Relatorio com um método concreto gerar() que monta o texto chamando dois métodos abstratos, cabecalho() e corpo(). Implemente RelatorioVendas, que recebe uma lista de vendas e mostra o total.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from abc import ABC, abstractmethod


class Relatorio(ABC):
    def gerar(self):  # método-modelo: define o roteiro
        return "\n".join([self.cabecalho(), self.corpo(), "-- fim --"])

    @abstractmethod
    def cabecalho(self):
        ...

    @abstractmethod
    def corpo(self):
        ...


class RelatorioVendas(Relatorio):
    def __init__(self, vendas):
        self.vendas = vendas

    def cabecalho(self):
        return "RELATÓRIO DE VENDAS"

    def corpo(self):
        return f"Total: R$ {sum(self.vendas):.2f}"


print(RelatorioVendas([100, 250.5, 49.5]).gerar())

Saída:

1
2
3
RELATÓRIO DE VENDAS
Total: R$ 400.00
-- fim --

Esse é o padrão de projeto Template Method: a mãe define a ordem dos passos e as filhas preenchem os detalhes. Um RelatorioEstoque precisaria implementar só cabecalho() e corpo().

Exercício 5. Crie a classe abstrata Forma com a propriedade abstrata area e um __lt__ que compara formas pela área. Implemente Quadrado e Circulo e use max() para achar a maior forma de uma lista.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from abc import ABC, abstractmethod
import math


class Forma(ABC):
    @property
    @abstractmethod
    def area(self):
        ...

    def __lt__(self, outra):
        return self.area < outra.area


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

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


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

    @property
    def area(self):
        return round(math.pi * self.raio ** 2, 2)


maior = max([Quadrado(3), Circulo(2), Quadrado(1)])
print(type(maior).__name__, maior.area)

Saída: Circulo 12.57

O __lt__ concreto da mãe funciona para qualquer filha, porque todas são obrigadas a ter area. O círculo de raio 2 (12,57) vence o quadrado de lado 3 (9).

Exercício 6. Crie a classe Semana, filha de collections.abc.Sequence, implementando só __getitem__ e __len__. Mostre que ela ganha in, index(), count() e reversed() sem você escrever esses métodos.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from collections.abc import Sequence


class Semana(Sequence):
    _dias = ["seg", "ter", "qua", "qui", "sex", "sáb", "dom"]

    def __getitem__(self, indice):
        return self._dias[indice]

    def __len__(self):
        return len(self._dias)


s = Semana()
print(len(s), s[0], s[-1])
print("qua" in s, s.index("sex"), s.count("dom"))
print(list(reversed(s))[:2])

Saída:

1
2
3
7 seg dom
True 4 1
['dom', 'sáb']

Sequence é uma classe abstrata com dois métodos abstratos (__getitem__ e __len__) e vários concretos construídos em cima deles. É o mesmo desenho do Pagamento deste post, só que vindo da biblioteca padrão.

Exercício 7. Reescreva o contrato “algo que sabe exportar(dados)” usando typing.Protocol em vez de ABC, de forma que uma classe ExportadorCsv seja aceita sem herdar de nada.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from typing import Protocol, runtime_checkable


@runtime_checkable
class Exportador(Protocol):
    def exportar(self, dados: dict) -> str:
        ...


class ExportadorCsv:  # não herda de nada
    def exportar(self, dados: dict) -> str:
        return ",".join(f"{k}={v}" for k, v in dados.items())


def salvar(exportador: Exportador, dados: dict) -> str:
    return exportador.exportar(dados)


print(salvar(ExportadorCsv(), {"nome": "Ana", "idade": 30}))
print(isinstance(ExportadorCsv(), Exportador))

Saída:

1
2
nome=Ana,idade=30
True

Com ABC, ExportadorCsv precisaria herdar de Exportador. Com Protocol, basta ter o método. A troca: o Python não impede a criação de uma classe incompleta, quem avisa é o mypy.

Exercício 8. Usando as classes Forma e Quadrado (incompleto, sem perimetro()) da seção sobre o TypeError, escreva um código que liste quais métodos ainda faltam implementar em Quadrado, sem tentar instanciá-lo.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from abc import ABC, abstractmethod
import inspect


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

    @abstractmethod
    def perimetro(self):
        ...


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

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


if inspect.isabstract(Quadrado):
    print("Falta implementar:", sorted(Quadrado.__abstractmethods__))

Saída: Falta implementar: ['perimetro']

__abstractmethods__ é um frozenset com os nomes dos métodos abstratos ainda pendentes. O sorted() transforma em lista ordenada, o que deixa a saída previsível.

Conclusão

Abstração é mostrar o essencial e esconder os detalhes. Em Python, você a torna explícita com classes abstratas: herde de abc.ABC, marque os métodos obrigatórios com @abstractmethod e o Python passa a recusar, com TypeError, qualquer tentativa de criar um objeto incompleto. Lembre que a mensagem desse erro mudou no Python 3.12, então trate a exceção pelo tipo, não pelo texto. Classes abstratas podem ter estado, métodos concretos e até métodos abstratos com código; quando você quer só um contrato sem herança, typing.Protocol é a alternativa.

Com este post, você fechou os quatro pilares da POO: encapsulamento (com @property), herança, polimorfismo e abstração. Para colocar tudo isso em prática em projetos guiados, conheça o nosso curso de Python completo.

Nos vemos no próximo post!

Perguntas frequentes

O que é abstração em programação orientada a objetos?

Abstração é modelar apenas o que é essencial de um objeto para quem vai usá-lo, escondendo os detalhes de implementação. Quem chama pagamento.processar() precisa saber o que o método faz, não como ele conversa com o banco. Em Python, a abstração aparece em classes com uma interface simples e, de forma explícita, nas classes abstratas do módulo abc.

O que é uma classe abstrata em Python?

É uma classe que serve de modelo para outras e não pode ser instanciada diretamente. Em Python ela é criada herdando de abc.ABC e marcando um ou mais métodos com o decorador @abstractmethod. Toda subclasse precisa implementar esses métodos para poder ser instanciada; caso contrário, o Python lança TypeError na criação do objeto.

Como criar uma classe abstrata em Python?

Importe com from abc import ABC, abstractmethod, faça a classe herdar de ABC (class Forma(ABC):) e decore os métodos obrigatórios com @abstractmethod. O corpo desses métodos pode ser só ... ou uma docstring. A classe também pode ter métodos concretos, que as filhas herdam prontos.

O que acontece ao instanciar uma classe abstrata em Python?

O Python lança TypeError na hora de criar o objeto. No Python 3.10 e 3.11 a mensagem é Can't instantiate abstract class Forma with abstract methods area, perimetro; a partir do 3.12 ela virou Can't instantiate abstract class Forma without an implementation for abstract methods 'area', 'perimetro'. O mesmo erro ocorre com uma subclasse que não implementou todos os métodos abstratos.

Python tem interface?

Não existe a palavra-chave interface como em Java ou C#. Em Python, uma interface é feita com uma classe abstrata que só tem métodos abstratos (herança obrigatória, verificação ao instanciar) ou com typing.Protocol (sem herança, verificação feita por ferramentas como o mypy). Classes abstratas também podem ter métodos concretos, o que as interfaces clássicas não têm.

Qual a diferença entre abstração e encapsulamento?

Abstração decide o que expor: define a interface essencial de um objeto (quais métodos existem e o que fazem). Encapsulamento decide como proteger: esconde os dados internos e controla o acesso a eles, em Python com a convenção _atributo e com @property. Os dois trabalham juntos, mas respondem perguntas diferentes.

Um método abstrato pode ter implementação em Python?

Pode. Diferente de Java, um método decorado com @abstractmethod pode ter corpo com código. A subclasse continua obrigada a sobrescrevê-lo, mas pode reaproveitar a implementação da mãe chamando super().metodo(). É uma forma de oferecer um comportamento padrão sem abrir mão da obrigatoriedade.

Sobre classes abstratas em Python, é correto afirmar que elas não podem ter métodos concretos?

Não, a afirmação é falsa. Uma classe abstrata criada com abc.ABC pode ter quantos métodos concretos quiser, além de __init__ e atributos. O que a torna abstrata é ter pelo menos um método marcado com @abstractmethod ainda não implementado. As filhas herdam os métodos concretos prontos e só precisam implementar os abstratos.

Começe agora sua Jornada na Programação!

Não deixe para amanhã o sucesso que você pode começar a construir hoje!

#newsletter Olá :wave: Curtiu o artigo? Então faça parte da nossa Newsletter! Privacidade Não se preocupe, respeitamos sua privacidade. Você pode se descadastrar a qualquer momento.