Duck typing en python

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!


Ver también