Blog

ABCs e Protocols em Python

O Python não verifica se um objeto tem o método que você está prestes a chamar. Isso é duck typing, e é por isso que código Python se combina tão facilmente — e por que um erro de digitação aparece num lugar pouco útil.

Onde duck typing dói

class JsonExporter:
    def export(self, rows): return f"json:{len(rows)}"
class CsvExporter:
    def export(self, rows): return f"csv:{len(rows)}"
class Broken:
    def exprot(self, rows): return "typo"     # misspelled

def run(exporter, rows): return exporter.export(rows)
print(run(JsonExporter(), [1,2]), run(CsvExporter(), [1,2]))
try:
    run(Broken(), [1,2])
except AttributeError as err:
    print(type(err).__name__ + ':', err)

Ele imprime:

json:2 csv:2
AttributeError: 'Broken' object has no attribute 'export'

Os dois bons funcionam sem nenhuma classe base em comum, que é o duck typing valendo a pena. O Broken falha — mas dentro do run, na hora da chamada, possivelmente em produção, e o traceback aponta para quem chamou em vez da classe com o erro de digitação.

Duas ferramentas resolvem isso, vindo de pontas opostas.

ABC: a classe promete antecipadamente

from abc import ABC, abstractmethod
class Exporter(ABC):
    @abstractmethod
    def export(self, rows): ...
    def describe(self): return f"{type(self).__name__} exporter"

class GoodExporter(Exporter):
    def export(self, rows): return f"good:{len(rows)}"

print(GoodExporter().describe(), GoodExporter().export([1,2]))

class BadExporter(Exporter):
    pass
try:
    BadExporter()
except TypeError as err:
    print(type(err).__name__ + ':', str(err)[:80])

Ele imprime:

GoodExporter exporter good:2
TypeError: Can't instantiate abstract class BadExporter without an implementation for abstr

A falha se mudou para a instanciação, que é bem mais cedo e bem mais claro. Uma ABC também pode entregar comportamento compartilhado — o describe aqui — que um Protocol não pode.

O custo: a classe precisa herdar da sua ABC. Você voltou a uma relação de herança, com tudo o que a parte 6 disse sobre isso.

Protocol: quem chama declara o que precisa

from typing import Protocol, runtime_checkable

@runtime_checkable
class SupportsExport(Protocol):
    def export(self, rows) -> str: ...

print('JsonExporter matches :', isinstance(JsonExporter(), SupportsExport))
print('Broken matches       :', isinstance(Broken(), SupportsExport))
print('JsonExporter subclasses SupportsExport?', issubclass(JsonExporter, SupportsExport))

Ele imprime:

JsonExporter matches : True
Broken matches       : False
JsonExporter subclasses SupportsExport? True

O JsonExporter nunca ouviu falar de SupportsExport e mesmo assim corresponde, porque tem o método certo. Isso é tipagem estrutural — o formato é o contrato. Verificadores de tipo garantem isso antes do código rodar; o @runtime_checkable ainda permite o isinstance.

Repare que a verificação em tempo de execução só confere se o método existe, não a assinatura dele. O verificador estático é quem faz o trabalho de verdade.

A diferença que decide

class ThirdParty:            # imagine this is in a library
    def export(self, rows): return f"vendor:{len(rows)}"

print('Protocol accepts it :', isinstance(ThirdParty(), SupportsExport))
print('ABC accepts it      :', isinstance(ThirdParty(), Exporter))

Ele imprime:

Protocol accepts it : True
ABC accepts it      : False

Você não consegue fazer uma classe de terceiros herdar da sua ABC. Você consegue perfeitamente escrever um Protocol que ela já satisfaz.

É essa a troca inteira:

  • ABC — você é dono de todas as implementações, e quer compartilhar comportamento além de exigir uma interface.
  • Protocol — as implementações vêm de fora, ou você só quer descrever o que uma função precisa sem exigir que alguém herde nada.

O que lembrar

  • Duck typing falha no ponto da chamada; as duas ferramentas antecipam a falha.

  • Uma ABC verifica na instanciação e pode carregar código compartilhado, mas exige herança.

  • Um Protocol é estrutural — qualquer coisa com o formato certo corresponde, inclusive código que você não controla.

  • @runtime_checkable só confere se os nomes dos métodos existem. Assinaturas são trabalho do verificador de tipo.

Para um parâmetro de função, prefira um Protocol: ele diz o que você precisa sem restringir quem pode fornecer. Use uma ABC quando você está construindo uma família de classes que são suas e que compartilham comportamento real.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.