El Duck typing es una forma de tipado estructural que permite definir interfaces de objetos, que no estando relacionados, se comportan de forma similar.
If it walks like a duck and it quacks like a duck, then it must be a duck
Es decir, si el objeto cumple con la interfaz definida, entonces es un pato (dando igual que sea un pato o una gallina).
Sin la ayuda de ningún artefacto, es muy sencillo de usar en Python porque es un lenguaje dinámico.
from typing import Any
def start(obj: Any) -> None:
obj.start()
class Airplane:
def start(self) -> None:
print("Starting airplane")
class Computer:
def start(self) -> None:
print("Starting computer")
an_airplane = Airplane()
start(an_airplane)
a_computer = Computer()
start(a_computer)
Si queremos añadir más seguridad a nuestro código y que no explote en tiempo de ejecución, podemos usar el método el método hasattr
.
from typing import Any
def start(obj: Any) -> None:
if hasattr(obj, "start"):
obj.start()
else:
raise TypeError("Object does not have a start method")
class Airplane:
def start(self) -> None:
print("Starting airplane")
class Computer:
def start(self) -> None:
print("Starting computer")
class Person:
pass
an_airplane = Airplane()
start(an_airplane)
a_computer = Computer()
start(a_computer)
a_person = Person()
start(a_person) # You can't start a person
Si usamos una clase base con un método abstracto, podemos resolverlo de la siguiente manera:
import abc
class Startable(abc.ABC):
@abc.abstractmethod
def start(self):
pass
def start(obj: Startable) -> None:
obj.start()
class Airplane(Startable):
def start(self) -> None:
print("Starting airplane")
class Computer(Startable):
def start(self) -> None:
print("Starting computer")
class Person:
pass
an_airplane = Airplane()
start(an_airplane)
a_computer = Computer()
start(a_computer)
Aquí no estoy llamado a la función start
con una instancia de Person
porque no hereda de Startable
y mypy
me ha avisado en tiempo de compilación error: Argument 1 to "start" has incompatible type "Person"; expected "Startable" [arg-type]
Sin embargo, no queremos convertir Python en un lenguaje estático, así que la solución final pasa por usar un protocolo, que es una especie de interfaz informal que se asume implementará un objeto (por ejemplo si el objeto tiene el método __len__
se dice implementa el protocolo __len__
y entonces puedes escribir len(obj)
.
Con typing.Protocol
podemos hacer explícito ese protocolo y en vez de usar el módulo abc
y establecer una relación de parentesco entre clases, podemos usar una clase base que define el protocolo sólo para cumplir con el Duck typing, una interfaz al fin y al cabo (aquí cabría recordar que en Python existe la herencia múltiple, no se implementan interfaces con una palabra clave estilo implements
o algo similar, simplemente se hereda).
No es tan potente como el tipado estructural de TypeScript, donde no hubiera hecho falta la clase Startable
ni que que Airplane
ni Computer
heredaran de Startable
, pero se le parece bastante.
La idea está sacada de Python Type Hints - Duck typing with Protocol, pero también te recomiendo la lectura de este otro post I Want A New Duck.
from typing import Protocol
class Startable(Protocol):
def start(self) -> None:
pass
def start(obj: Startable) -> None:
obj.start()
class Airplane(Startable):
def start(self) -> None:
print("Starting airplane")
class Computer(Startable):
def start(self) -> None:
print("Starting computer")
class Person:
pass
an_airplane = Airplane()
start(an_airplane)
a_computer = Computer()
start(a_computer)
De nuevo, mypy
avisó de que Person
no cumple con la interfaz, error: Argument 1 to "start" has incompatible type "Person"; expected "Startable" [arg-type]
¡Un saludo!