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_checkablesó 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.