Python no comprueba que un objeto tenga el método que estás por llamar. Eso es duck typing, y es la razón por la que el código en Python se combina con tanta facilidad, y también por la que un error de tipeo aparece en un lugar poco útil.
Dónde duele el duck typing
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)
Imprime:
json:2 csv:2
AttributeError: 'Broken' object has no attribute 'export'
Los dos buenos funcionan sin ninguna clase base compartida, que es el duck typing ganándose el sueldo. Broken falla, pero dentro de run, en el momento de la llamada, quizá en producción, y el traceback apunta a quien llamó en lugar de a la clase que tiene el error de tipeo.
Dos herramientas arreglan eso, desde extremos opuestos.
ABC: la clase promete de antemano
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])
Imprime:
GoodExporter exporter good:2
TypeError: Can't instantiate abstract class BadExporter without an implementation for abstr
La falla se movió al momento de instanciar, que es mucho antes y mucho más claro. Una ABC además puede traer comportamiento compartido —describe, aquí— cosa que un Protocol no puede.
El costo: la clase tiene que heredar de tu ABC. Vuelves a una relación de herencia, con todo lo que dijo la parte 6 al respecto.
Protocol: quien llama declara lo que necesita
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))
Imprime:
JsonExporter matches : True
Broken matches : False
JsonExporter subclasses SupportsExport? True
JsonExporter nunca supo de la existencia de SupportsExport y aun así calza, porque tiene el método correcto. Esto es tipado estructural: la forma es el contrato. Los verificadores de tipos lo exigen antes de que el código corra; @runtime_checkable además permite usar isinstance.
Ojo: la comprobación en tiempo de ejecución solo verifica que el método exista, no su firma. El trabajo de verdad lo hace el verificador estático.
La diferencia que lo 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))
Imprime:
Protocol accepts it : True
ABC accepts it : False
No puedes hacer que una clase de terceros herede de tu ABC. Sí puedes, sin problema, escribir un Protocol que ya cumpla.
Ese es todo el intercambio:
- ABC — tú eres dueño de todas las implementaciones, y quieres compartir comportamiento además de exigir una interfaz.
- Protocol — las implementaciones vienen de otro lado, o simplemente quieres describir lo que una función necesita sin obligar a nadie a heredar nada.
Qué recordar
-
El duck typing falla en el lugar de la llamada; las dos herramientas adelantan esa falla.
-
Una ABC exige al instanciar y puede llevar código compartido, pero obliga a heredar.
-
Un Protocol es estructural: cualquier cosa con la forma correcta calza, incluido código que no controlas.
-
@runtime_checkablesolo comprueba que existan los nombres de los métodos. Las firmas son trabajo del verificador de tipos.
Para un parámetro de función, prefiere un Protocol: dice lo que necesitas sin limitar quién puede proveerlo. Recurre a una ABC cuando estés construyendo una familia de clases tuyas que comparten comportamiento real.