Sobrecarga de funciones o despacho múltiple en Python

Sobrecarga de funciones o despacho múltiple en Python



En algunos lenguajes de programación, como Java o C++, es posible definir múltiples funciones con un mismo nombre, siempre y cuando las definiciones difieran en la cantidad de argumentos o bien en el tipo de dato de alguno de ellos. A esta práctica se la denomina sobrecarga de funciones (o bien métodos, en caso de tratarse de una clase).

Consideremos, por ejemplo, el siguiente código de C++.

int add(int a, int b)
{
    return a + b;
}
float add(float a, float b)
{
    return a + b;
}

Al momento de llamar a la función add(), el compilador determinará a cuál de las dos definiciones corresponde, teniendo en cuenta los argumentos que se hayan pasado.

int main()
{
    // Primera definición.
    std::cout << add(7, 5) << std::endl;
    // Segunda definición.
    std::cout << add(1.5f, 3.7f) << std::endl;
    return 0;
}

Este tipo de práctica no es posible per se en Python, habida cuenta de que la elección de una u otra función es determinada en base al tipo de dato esperado por cada uno de los argumentos y de que Python es un lenguaje de tipado dinámico (esto es, no se indica el tipo de dato de un objeto al declararlo).

Si definimos dos funciones con el mismo nombre no obtendremos ningún error, pero prevalecerá aquella que se haya definido en segundo lugar.

def saludo():
    print("¡Hola, mundo!")

def saludo():
    print("¡Hello, world!")

saludo()  # ¡Hello, world!

No obstante, podemos conseguir el mismo resultado que los lenguajes que soportan sobrecarga de funciones de diversas maneras. Tomemos como referencia la función incorporada range(). Notaremos que pareciera que está definida de múltiples maneras, dependiendo de la cantidad de argumentos que se le pasen: si le pasamos un único valor, retorna números del 0 hasta dicho valor; si le pasamos dos, valores del primero hasta el segundo.

>>> list(range(5))
[0, 1, 2, 3, 4]
>>> list(range(7, 12))
[7, 8, 9, 10, 11]

Para que sea más explícito: el primer argumento denota hasta qué valor se construirá la lista resultante cuando se haya pasado un solo argumento; en caso de haber dos, el primer argumento indica el inicio.

Tratemos de reponer este comportamiento imitando el funcionamiento de range() con una función propia.

def range2(start, end):
    ret = []
    while start < end:
        ret.append(start)
        start += 1
    return ret

print(range2(7, 12))  # [7, 8, 9, 10, 11]

Aquí hemos definido nuestra range2() que se corresponde solamente con una de las acepciones de la original range(): aquella que toma dos argumentos. Podemos incluir la otra acepción haciendo de end un argumento opcional.

def range2(start, end=None):
    if end is None:
        end = start
        start = 0
    ret = []
    while start < end:
        ret.append(start)
        start += 1
    return ret

print(range2(5))      # [0, 1, 2, 3, 4]
print(range2(7, 12))  # [7, 8, 9, 10, 11]

De este modo obtenemos el mismo resultado que el provisto por la sobrecarga de funciones en otros lenguajes. Ahora bien, hay otras soluciones, como la siguiente.

def range2(*args):
    if len(args) == 1:
        start = 0
        end = args[0]
    elif len(args) == 2:
        start, end = args
    else:
        raise TypeError
    ret = []
    while start < end:
        ret.append(start)
        start += 1
    return ret

En este caso hacemos que nuestra función acepte cualquier cantidad de argumentos; luego, dependiendo de los que se hayan pasado, realizamos una acción u otra. Si se ha excedido, lanzamos un error. Para mayor claridad, podemos aun expresarlo así:

def range2_from_to(start, end):
    ret = []
    while start < end:
        ret.append(start)
        start += 1
    return ret

def range2_to(end):
    return range2_from_to(0, end)

def range2(*args):
    if len(args) == 1:
        return range2_to(*args)
    elif len(args) == 2:
        return range2_from_to(*args)
    else:
        raise TypeError

Ahora bien, esta mímesis de la sobrecarga de funciones que hemos hecho se basa en el número de argumentos pasados. ¿Qué sucede si queremos definir distintas implementaciones de una misma función según el tipo de dato de sus argumentos? Bien podemos chequearlo usando isinstance().

def add(a, b):
    if isinstance(a, int) and isinstance(b, int):
        return a + b
    elif isinstance(a, str) and isinstance(b, str):
        return " ".join((a, b))
    else:
        raise TypeError

print(add(7, 5))  # 12
print(add("¡Hola,", "mundo!"))  # ¡Hola, mundo!

A esta operación se la conoce como despacho múltiple. Algunas soluciones escritas por la comunidad son un tanto más legibles, por ejemplo, el módulo multipledispatch (pip install multipledispatch), que ofrece una sintaxis clara usando decoradores:

from multipledispatch import dispatch

@dispatch(int, int)
def add(a, b):
    return a + b

@dispatch(str, str)
def add(a, b):
    return " ".join((a, b))

Por otro lado, incluso la librería estándar provee un decorador similar llamado functools.singledispatch, aunque solamente opera sobre funciones con un único argumento.

Con esto concluimos que, si bien Python no provee mecanismos a nivel lenguaje para la sobrecarga de funciones ni el despacho múltiple, bien puede obtenerse el mismo resultado de diversas maneras (y esta es precisamente la razón para no incluirlo como una característica del lenguaje): simplemente selecciona la que te resulte más agradable.



Deja un comentario