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 comsuper()). - Python não tem
interface: usa ABC (com herança) outyping.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! ![]()
Vá Direto ao Assunto…
- O que é abstração na programação orientada a objetos
- Classes abstratas com abc.ABC e @abstractmethod
- TypeError ao instanciar uma classe abstrata
- Métodos abstratos com implementação e propriedades abstratas
- Interface vs classe abstrata em Python
- Quando usar classe abstrata
- Erros comuns
- Exercícios resolvidos
- Conclusão
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 (oCartaoestende comsuper().__init__(valor), como vimos no post de herança). - Um método abstrato,
processar(), cujo corpo é só uma docstring. Também é comum usar...oupass. - Um método concreto,
recibo(), que chama o método abstrato. Ele funciona em qualquer filha porque todas são obrigadas a terprocessar(). 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? ![]()
Que tal receber 30 dias de conteúdo direto na sua Caixa de Entrada?
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
Pagamentoacima. -
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.
"Porque o Senhor dá a sabedoria, e da sua boca vem a inteligência e o entendimento" Pv 2:6