Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

En Python las funciones son “ciudadanos de primera clase”, lo que le otorga al lenguaje ciertas características del paradigma funcional. En este capítulo, exploraremos cómo definir y utilizar funciones, los diferentes tipos de pasajes de parámetros, y cómo Python implementa el paradigma funcional a través de funciones de orden superior, funciones anónimas (lambda), y más.

1Pasajes de parámetros y devolución de valores

Las funciones se definen utilizando la palabra clave def, seguida del nombre de la función y paréntesis que pueden (o no) contener parámetros. Las funciones pueden retornar valores utilizando la palabra clave return.

Python permite pasar varios tipos de parámetros a funciones:

def funcion(posicionales, nombrados, *posicionales_variables, **nombrados_variables):
    pass  # sentencia que no hace nada
Posicionales
Los parámetros se deben pasan en el orden en que están definidos.
Nombrados
Los parámetros se pasan utilizando su nombre, lo que permite especificar solo algunos de ellos.
Posicionales variables
Se pueden pasar un número variable de argumentos posicionales utilizando *.
Nombrados variables
Se pueden pasar un número variable de argumentos nombrados utilizando **.

1.1Parámetros posicionales

Los parámetros se ligan con los argumentos de la función en el orden en que están definidos. Si se pasan menos argumentos de los esperados, se generará un error.

def concatenar_cadenas(cadena1, cadena2):
    return cadena1 + cadena2


print(concatenar_cadenas("Hola, ", "mundo!"))
print(concatenar_cadenas("mundo!", "Hola, "))
Output
Hola, mundo!
mundo!Hola, 

1.2Parámetros nombrados

Al momento de invocar la función se pueden nombrar los parámetros y de esa forma no es necesario respetar el orden de los parámetros. Esto es útil cuando se tienen muchos parámetros y se quiere especificar solo algunos.

def concatenar_cadenas(cadena1, cadena2):
    return cadena1 + cadena2


print(concatenar_cadenas(cadena2="mundo!", cadena1="Hola, "))
Output
Hola, mundo!

1.3Parámetros posicionales variables

Los parámetros posicionales variables se definen utilizando un asterisco (*) antes del nombre del parámetro. Esto permite pasar un número variable de argumentos posicionales a la función. Python internamente agrupa estos argumentos en una tupla.

def sumar(*numeros):
    print(f"{sumar.__name__}: Se recibieron {len(numeros)} numeros")
    print(f"{sumar.__name__}: El tipo de numeros es: {type(numeros)}")
    suma = 0
    for num in numeros:
        suma += num
    return suma


print(sumar(1, 2, 3))
print(sumar(4, 5, 6, 7, 8))
Output
sumar: Se recibieron 3 numeros
sumar: El tipo de numeros es: <class 'tuple'>
6
sumar: Se recibieron 5 numeros
sumar: El tipo de numeros es: <class 'tuple'>
30

1.4Parámetros nombrados variables

Los parámetros nombrados variables se definen utilizando dos asteriscos (**) antes del nombre del parámetro. Esto permite pasar un número variable de argumentos nombrados a la función. Python internamente agrupa estos argumentos en un diccionario.

def mostrar_info(**info):
    print(f"{mostrar_info.__name__}: Se recibieron {len(info)} argumentos nombrados.")
    print(f"{mostrar_info.__name__}: El tipo de `info` es: {type(info)}")
    print(f"{mostrar_info.__name__}: {info = }")


mostrar_info(nombre="Juan", edad=30, ciudad="Madrid")
Output
mostrar_info: Se recibieron 3 argumentos nombrados.
mostrar_info: El tipo de `info` es: <class 'dict'>
mostrar_info: info = {'nombre': 'Juan', 'edad': 30, 'ciudad': 'Madrid'}

1.5Parámetros por defecto

Los parámetros por defecto permiten definir valores predeterminados para los parámetros de una función. Si no se pasa un argumento para ese parámetro, se utilizará el valor por defecto.

def saludar(nombre="mundo"):
    return f"Hola, {nombre}!"


print(saludar())
print(saludar("Juan"))
Output
Hola, mundo!
Hola, Juan!

Por ejemplo:

def funcion_ejemplo(param1, param2="valor_por_defecto", *args, **kwargs):
    print(f"param1: {param1}, param2: {param2}")
    print(f"args: {args}")
    print(f"kwargs: {kwargs}")
funcion_ejemplo(1, 2, 3, 4, clave1="valor1", clave2="valor2")
Output
param1: 1, param2: 2
args: (3, 4)
kwargs: {'clave1': 'valor1', 'clave2': 'valor2'}

En el siguiente ejemplo args queda ligado a la tupla vacía, ya que param2 quedará ligado a la tupla (2, 3).

funcion_ejemplo(1, (2, 3), clave1="valor1", clave2="valor2")
Output
param1: 1, param2: (2, 3)
args: ()
kwargs: {'clave1': 'valor1', 'clave2': 'valor2'}

1.6Devolución de valores

Las funciones pueden devolver valores utilizando la palabra clave return. Si no se especifica un valor de retorno, la función devolverá None por defecto. En Python, una función puede devolver múltiples valores separados por comas, que se empaquetan en una tupla.

def division_y_resto(dividendo, divisor):
    cociente = dividendo // divisor
    resto = dividendo % divisor
    return cociente, resto


cociente, resto = division_y_resto(10, 3)

print(f"Cociente: {cociente}, Resto: {resto}")
Output
Cociente: 3, Resto: 1

Como Python tiene tipado dinámico, no es necesario especificar el tipo de retorno de una función. Sin embargo, se pueden utilizar anotaciones de tipo para documentar el tipo esperado de los parámetros y el valor de retorno.

def sumar(a: int, b: int) -> int:
    """
    Suma dos números enteros y devuelve el resultado.
    """
    return a + b


print(sumar(3, 5))
print(sumar("a", "b"))
Output
8
ab

Las anotaciones de tipo son opcionales y no afectan el comportamiento de la función, pero pueden ser útiles para la documentación y la verificación de tipos en tiempo de desarrollo.

En el fragmento anterior, la función sumar está anotada para indicar que espera dos enteros como parámetros y devuelve un entero. Sin embargo, Python no impone estas restricciones en tiempo de ejecución, por lo que se pueden pasar otros tipos de datos sin generar un error.

2Paradigma funcional

La programación funcional es un paradigma de programación declarativo basado en el uso de funciones verdaderamente matemáticas. En este estilo de programación las funciones son ciudadanas de primera clase, porque sus expresiones pueden ser asignadas a variables como se haría con cualquier otro valor; las funciones también pueden crearse en tiempo de ejecución, por ejemplo dentro de otra función.

En el paradigma funcional en general (y a diferencia del imperativo), la programación consiste en especificar el Qué queremos resolver y no el Cómo se resuelve el problema.

Por ejemplo:

def componer(func1, func2):
    def funcion_compuesta(x):
        return func2(func1(x))

    return funcion_compuesta


def cuadrado(x):
    return x * x


def doble(x):
    return x + x


doble_cuadrado = componer(cuadrado, doble)

doble_cuadrado(3)
Output
18

En este ejemplo, componer es una función de orden superior que toma dos funciones como argumentos y devuelve una nueva función que es la composición de las dos. Para poder elevar un número al cuadrado y luego duplicarlo, se utiliza doble_cuadrado, que es el resultado de componer cuadrado y doble. Hay que prestar atención al orden en que se componen las funciones, ya que se aplica primero cuadrado y luego doble.

2.1Funciones anónimas (lambda)

Las funciones anónimas, también conocidas como expresiones lambda, son funciones sin nombre que se definen utilizando la palabra clave lambda. Son útiles para crear funciones pequeñas y rápidas sin necesidad de definirlas formalmente.

suma = lambda x, y: x + y

suma(3, 5)
Output
8

Define una función que recibe dos argumentos x e y y retorna su suma. Esta función queda asociada a la variable suma, que se puede utilizar para invocarla.

El fragmento anterior de composición de funciones también se puede reescribir utilizando funciones lambda:

def componer(func1, func2):
    return lambda x: func2(func1(x))


doble_cuadrado = componer(lambda x: x * x, lambda x: x + x)

doble_cuadrado(3)
Output
18

Las funciones de orden superior, las funciones anónimas, la generación de datos por comprensión y las clausuras son características del paradigma funcional que hacen de Python un lenguaje versátil y poderoso. Algunos de los usos habituales de la programación funcional en Python incluyen:

Mapeo
Aplicar una función a cada elemento de una colección.

La función que mapea los elementos de una colección a otra debe ser una función que toma un solo argumento y devuelve un valor.

def mapear(func, iterable):
    """
    Aplica la función `func` a cada elemento de `iterable`
    y devuelve una lista con los resultados.
    """
    return [func(x) for x in iterable]


numeros = [x for x in range(10)]
cuadrados = mapear(lambda x: x**2, numeros)

print(f"Cuadrados: {cuadrados}")
Output
Cuadrados: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Python proporciona la función map para realizar mapeo de manera más concisa y que permite devolver un iterador en lugar de una lista. Como todo iterador, una vez que se consume, es decir que se itera sobre él, no se puede volver a utilizar. Por lo tanto, es común convertirlo a una lista o tupla para conservar los resultados.

numeros = [x for x in range(10)]
cuadrados = map(lambda x: x**2, numeros)

print(type(cuadrados))

tupla = tuple(cuadrados)

print(f"Cuadrados: {tupla}")
Output
<class 'map'>
Cuadrados: (0, 1, 4, 9, 16, 25, 36, 49, 64, 81)
Filtrado
Seleccionar elementos de una colección que cumplen con una condición.

La función que filtra los elementos de una colección debe ser una función que toma un solo argumento y devuelve un valor booleano.

def filtrar(func, iterable):
    """
    Filtra los elementos de `iterable` que cumplen con la
    condición definida en `func`.
    """
    return [x for x in iterable if func(x)]


numeros = [x for x in range(10)]
pares = filtrar(lambda x: x % 2 == 0, numeros)

print(f"Números pares: {pares}")
Output
Números pares: [0, 2, 4, 6, 8]

En este caso la función de filtrado es una función anónima lambda x: x % 2 == 0. Las funciones anónimas siempre devuelven el resultado de la última expresión evaluada, por lo que no es necesario utilizar return.

Python también proporciona la función filter para realizar filtrado de manera más concisa. filter devuelve un iterador que contiene los elementos de la colección que cumplen con la condición especificada por la función de filtrado.

numeros = [x for x in range(10)]
pares = filter(lambda x: x % 2 == 0, numeros)

print(type(pares))

lista = list(pares)  # Convierte el iterador a lista

print(f"Números pares: {lista}")
Output
<class 'filter'>
Números pares: [0, 2, 4, 6, 8]
Reducción
Combinar los elementos de una colección en un solo valor.

El módulo functools proporciona la función reduce, que es una función de orden superior que puede aplicar una función de reducción a los elementos de una colección, combinándolos en un solo valor. La función de reducción debe tomar dos argumentos y devolver un solo valor.

En este caso, se utiliza para sumar todos los números de la lista. Aplica la suma a los dos primeros elementos, luego aplica la suma al resultado y el siguiente elemento, y así sucesivamente hasta que se procesan todos los elementos de la lista.

from functools import reduce

numeros = [x for x in range(10)]
suma_total = reduce(lambda x, y: x + y, numeros)

print(f"Suma total: {suma_total}")
Output
Suma total: 45

La función de reducción es la función anónima lambda x, y: x + y, que toma dos argumentos y devuelve su suma. No hace falta utilizar return ya que la última expresión evaluada es justamente la suma de x e y.

Otro ejemplo de reducción con cadenas de caracteres:

from functools import reduce

palabras = ["Python", "mundo", "Hola"]
frase = reduce(lambda x, y: y + " " + x, palabras)

print(f"Frase: {frase}")
Output
Frase: Hola mundo Python

En este caso, la función de reducción concatena las palabras en orden inverso, creando una frase a partir de la lista de palabras. El orden inverso se debe al orden en que concatena los elementos la función de reducción.

3Funciones avanzadas

Iteradores
Son objetos que permiten recorrer una secuencia de elementos uno a uno. En Python todas las colecciones son iterables, lo que significa que se pueden recorrer directamente utilizando un bucle for o se puede obtener un iterador con iter y luego utilizar la función next para obtener cada uno de los valores. Cuando next no tiene más elementos para devolver, lanza una excepción StopIteration. En los iteradores de Python no hay una función has_next como en otros lenguajes, sino que utiliza excepciones para detectar el final de la iteración.
numeros = [x for x in range(10)]
iterador = iter(numeros)

while True:
    try:
        numero = next(iterador)
        print(numero)
    except StopIteration:
        break
Output
0
1
2
3
4
5
6
7
8
9
Decoradores
Son funciones que modifican el comportamiento de otras funciones. Se utilizan para agregar funcionalidades adicionales a funciones existentes sin modificar su código.
def decorador(func):
    """
    Decora la función `func` para agregarle mensajes al valor de retorno.
    """

    def funcion_decorada(*args, **kwargs):
        resultado = f"El resultado de la operación es: {func(*args, **kwargs)}"
        return resultado

    return funcion_decorada


def funcion_original(x):
    return x * 2


funcion_decorada = decorador(funcion_original)

funcion_decorada(5)
Output
'El resultado de la operación es: 10'

La función decorador toma una función func como argumento y devuelve una nueva función, funcion_decorada que agrega el mensaje El resultado de la operación es: al resultado de la función original.

Python proporciona una sintaxis especial para aplicar decoradores a funciones utilizando el símbolo @ antes de la definición de la función. Esto es equivalente a decorar la función manualmente como se mostró anteriormente.

def decorador(func):
    def funcion_decorada(*args, **kwargs):
        resultado = f"El resultado de la operación es: {func(*args, **kwargs)}"
        return resultado

    return funcion_decorada
@decorador
def funcion_original(x):
    return x * 2


funcion_original(5)
Output
'El resultado de la operación es: 10'
@decorador
def funcion_suma(a, b):
    return a + b


funcion_suma(3, 4)
Output
'El resultado de la operación es: 7'
Generadores
Son funciones que permiten crear iteradores de manera eficiente. Utilizan la palabra clave yield para devolver un valor y pausar la ejecución de la función, permitiendo que se reanude más tarde desde el mismo punto.
def contador():
    i = 0

    while True:
        yield i
        i += 1


contador_gen = contador()

print(type(contador_gen))

print(next(contador_gen))
print(next(contador_gen))
print(next(contador_gen))
Output
<class 'generator'>
0
1
2

Este generador nos permite, de alguna manera, tener una lista infinita de números enteros, ya que cada vez que se llama a next, se obtiene el siguiente número en la secuencia, lo cual es posible gracias a la palabra clave yield, que suspende la ejecución de la función en ese punto, devuelve el valor actual de i, y guarda el estado de la función para que pueda reanudarse en la siguiente llamada a next. En este caso next no levantará una excepción StopIteration porque el generador está diseñado para ser infinito.

También se puede utilizar la clausura para obtener un comportamiento similar a los generadores:

def contador():
    i = 0

    def siguiente():
        nonlocal i
        valor = i
        i += 1
        return valor

    return siguiente


siguiente = contador()

print(siguiente())
print(siguiente())
print(siguiente())
Output
0
1
2

La clave está en utilizar nonlocal para modificar la variable i dentro de la función interna siguiente, permitiendo que se mantenga el estado entre llamadas. i se almacena en la clausura de siguiente, lo que permite que su valor persista entre invocaciones.

4Recursos para profundizar