5.4 Funciones de orden superior
En Python, las funciones son ciudadanos de primera clase (first-class objects), lo que significa que se pueden almacenar en variables, pasar como argumentos y devolver como resultado de otras funciones. Este comportamiento permite crear las llamadas funciones de orden superior, es decir, funciones que reciben otras funciones como parámetro o las devuelven como resultado.
Este concepto procede de la programación funcional y ofrece una gran flexibilidad a la hora de estructurar el código.
1️⃣ Qué es una función de orden superior
Una función de orden superior es aquella que:
- Recibe una función como argumento, o
- Devuelve una función como resultado, o ambas cosas a la vez.
Gracias a este mecanismo, Python permite construir estructuras más expresivas y reutilizables.
def aplicar_operacion(funcion, valor):
"""Aplica la función recibida al valor dado."""
return funcion(valor)
def cuadrado(x):
return x ** 2
print(aplicar_operacion(cuadrado, 5))
- La función
aplicar_operacion()recibe como argumento otra función (cuadrado) y un valor (5). - Dentro de
aplicar_operacion()se ejecutafuncion(valor), que en este caso equivale acuadrado(5), devolviendo25. - De este modo, una función puede actuar sobre otras funciones sin conocer su implementación concreta.
2️⃣ Funciones como objetos de primera clase
En Python, las funciones son tratadas como cualquier otro tipo de dato. Esto significa que se pueden:
- Asignar a variables,
- Pasar como parámetros,
- Guardar en estructuras de datos,
- Y devolver como resultado de otra función.
def sumar(a, b):
return a + b
def restar(a, b):
return a - b
def multiplicar(a, b):
return a * b
operaciones = [sumar, restar, multiplicar]
for operacion in operaciones:
print(operacion(5, 3))
- Cada elemento de la lista
operacioneses una referencia a una función. - El bucle
forrecorre la lista y ejecuta cada una, mostrando los resultados:8,2y15. - Esta técnica resulta útil para aplicar un conjunto de operaciones de forma dinámica.
3️⃣ Funciones que devuelven otras funciones
Una función de orden superior también puede devolver otra función. Esto se utiliza, por ejemplo, para crear funciones personalizadas o configuradas dinámicamente.
def crear_multiplicador(factor):
"""Devuelve una nueva función que multiplica por el factor dado."""
def multiplicar(numero):
return numero * factor
return multiplicar
por_dos = crear_multiplicador(2)
por_tres = crear_multiplicador(3)
print(por_dos(10))
print(por_tres(10))
- La función
crear_multiplicador()devuelve otra función (multiplicar) que recuerda el valor defactor. - Cuando se llama a
por_dos(10), devuelve20, ypor_tres(10)devuelve30.
Este comportamiento se conoce como closure (clausura), y permite mantener valores en memoria incluso después de que la función exterior haya finalizado.
4️⃣ Funciones de orden superior incorporadas
Python incluye varias funciones de orden superior muy utilizadas que ya hemos presentado en el punto anterior: map(), filter() y reduce().
Estas funciones toman otra función como argumento para aplicar transformaciones o filtrados sobre colecciones de datos.
🟩 map()
map(funcion, iterable) aplica la función especificada a cada elemento del iterable y devuelve un nuevo objeto de tipo map.
def elevar_cuadrado(x):
return x ** 2
numeros = [1, 2, 3, 4, 5]
resultado = list(map(elevar_cuadrado, numeros))
print(resultado)
- Cada elemento de la lista
numerosse eleva al cuadrado mediante la funciónelevar_cuadrado. - El resultado final es
[1, 4, 9, 16, 25].
Esta misma operación podría realizarse con una función lambda para simplificar aún más el código.
🟦 filter()
filter(funcion, iterable) recorre una colección y conserva solo los elementos que hacen que la función devuelva True.
def es_par(n):
return n % 2 == 0
numeros = [10, 15, 20, 25, 30]
pares = list(filter(es_par, numeros))
print(pares)
- La función
es_par()devuelveTruesolo para los números pares. filter()aplica esa función a cada elemento y devuelve una nueva lista con los que cumplen la condición:[10, 20, 30].
🟨 reduce()
reduce(funcion, iterable) aplica una función acumulativa a los elementos de una secuencia.
A diferencia de map() y filter(), no está incorporada directamente, sino que pertenece al módulo functools.
from functools import reduce
def multiplicar(x, y):
return x * y
numeros = [1, 2, 3, 4, 5]
resultado = reduce(multiplicar, numeros)
print(resultado)
reduce()aplica la funciónmultiplicaracumulativamente sobre la lista.- Primero multiplica
1×2, luego el resultado por3, y así sucesivamente, hasta obtener120.
Es una herramienta poderosa para combinar elementos en un único resultado.
5️⃣ Creación de funciones personalizadas con closures
Los closures son funciones internas que conservan el estado de las variables de la función exterior incluso después de que esta haya terminado. Se utilizan para crear funciones configuradas que recuerdan un valor o contexto determinado.
def contador():
"""Devuelve una función que incrementa un contador interno."""
n = 0
def incrementar():
nonlocal n
n += 1
return n
return incrementar
c = contador()
print(c()) # 1
print(c()) # 2
print(c()) # 3
- La variable
npertenece al ámbito decontador()y se conserva gracias al uso denonlocal. - Cada vez que se llama a
c(), el valor interno dense incrementa, manteniéndose entre ejecuciones.
Esto demuestra que las funciones pueden retener un “estado interno” sin usar variables globales.
6️⃣ Ejemplos prácticos de funciones de orden superior
🟩 Ejemplo 1: transformación de datos
def aplicar(funcion, datos):
"""Aplica una función a cada elemento de una lista."""
return [funcion(x) for x in datos]
numeros = [1, 2, 3, 4]
resultado = aplicar(lambda x: x ** 3, numeros)
print(resultado)
- La función
aplicar()recibe otra función (lambda x: x ** 3) y una lista de números. - Devuelve una nueva lista con cada número elevado al cubo.
🟦 Ejemplo 2: filtrado condicional
def filtrar(funcion, datos):
"""Devuelve una lista con los elementos que cumplen la condición."""
return [x for x in datos if funcion(x)]
valores = [5, 8, 13, 21, 34]
pares = filtrar(lambda x: x % 2 == 0, valores)
print(pares)
- La función
filtrar()recibe como argumento una función que comprueba si un número es par. - El resultado será
[8, 34].
🟨 Ejemplo 3: devolver funciones configuradas
def potencia(n):
"""Devuelve una función que eleva al exponente n."""
def elevar(x):
return x ** n
return elevar
al_cuadrado = potencia(2)
al_cubo = potencia(3)
print(al_cuadrado(5)) # 25
print(al_cubo(2)) # 8
potencia()genera nuevas funciones personalizadas: una que eleva al cuadrado y otra al cubo.- Cada una conserva el valor de
ngracias al mecanismo de cierre (closure).
7️⃣ Beneficios de las funciones de orden superior
Ventajas principales:
- Permiten mayor modularidad: el comportamiento se pasa como parámetro.
- Facilitan la reutilización del código.
- Favorecen un estilo de programación más declarativo y expresivo.
- Reducen la necesidad de bucles explícitos.
Ejemplo :
from functools import reduce
numeros = [1, 2, 3, 4, 5]
resultado = reduce(
lambda x, y: x + y,
filter(lambda n: n % 2 == 0, map(lambda n: n ** 2, numeros))
)
print(resultado)
map(lambda n: n ** 2, numeros)genera los cuadrados →[1, 4, 9, 16, 25].filter(lambda n: n % 2 == 0, ...)conserva solo los pares →[4, 16].reduce(lambda x, y: x + y, ...)suma los valores →20. Este ejemplo combina tres funciones de orden superior en una única línea clara y compacta.
8️⃣ Decoradores
Los decoradores son una de las aplicaciones más potentes y elegantes de las funciones de orden superior en Python. Un decorador es una función que recibe otra función como argumento y devuelve una nueva función, generalmente modificando o ampliando su comportamiento sin alterar su código original.
Se utilizan con frecuencia para:
- Registrar o medir la ejecución de funciones.
- Controlar el acceso a recursos.
- Validar parámetros.
- Implementar patrones como logging, caching o autenticación.
🟩 Concepto básico
En esencia, un decorador es una función que envuelve otra función. Esto se conoce como el patrón de envoltura (wrapper pattern).
def decorador(funcion):
"""Ejemplo de decorador que muestra mensajes antes y después."""
def envoltura():
print("Antes de ejecutar la función...")
funcion()
print("Después de ejecutar la función...")
return envoltura
def saludo():
print("¡Hola, mundo!")
saludo = decorador(saludo)
saludo()
# Antes de ejecutar la función...
# ¡Hola, mundo!
# Después de ejecutar la función...
decorador()recibe la funciónsaludocomo argumento.- Dentro de
decorador()se defineenvoltura(), que añade mensajes antes y después de llamar a la función original. - Finalmente,
decorador()devuelve la funciónenvoltura(). - Al reasignar
saludo = decorador(saludo), la llamadasaludo()ya no ejecuta directamente la función original, sino la versión decorada.
🟦 Uso de la sintaxis @decorador
Python ofrece una sintaxis simplificada para aplicar decoradores mediante el símbolo @.
En lugar de reasignar manualmente la función, se coloca @nombre_del_decorador justo encima de la definición.
def decorador(funcion):
def envoltura():
print("Ejecutando decorador...")
funcion()
print("Fin de la ejecución.")
return envoltura
@decorador
def saludo():
print("Hola desde la función decorada.")
saludo()
El decorador @decorador equivale a escribir saludo = decorador(saludo) justo después de definir la función.
Esta forma es más legible y habitual en el código Python moderno.
🟨 Decoradores con argumentos
Si la función original recibe argumentos, el decorador debe estar preparado para capturarlos y reenviarlos correctamente mediante *args y **kwargs.
def decorador(funcion):
def envoltura(*args, **kwargs):
print("Llamando a la función con argumentos:", args, kwargs)
resultado = funcion(*args, **kwargs)
print("Ejecución finalizada.")
return resultado
return envoltura
@decorador
def sumar(a, b):
print(f"La suma es: {a + b}")
return a + b
resultado = sumar(3, 5)
print("Resultado devuelto:", resultado)
- El decorador ahora puede interceptar los parámetros con
*argsy**kwargs, imprimirlos o modificarlos, y finalmente pasarlos a la función original. - El decorador también puede devolver el resultado original (
resultado), lo que lo hace completamente transparente para quien use la función.
🟧 Decoradores anidados
Una función puede tener varios decoradores aplicados a la vez. En ese caso, se ejecutan en orden ascendente desde el más cercano a la definición de la función original.
def mayusculas(funcion):
def envoltura():
return funcion().upper()
return envoltura
def signos_exclamacion(funcion):
def envoltura():
return f"¡{funcion()}!"
return envoltura
@signos_exclamacion
@mayusculas
def saludo():
return "hola mundo"
print(saludo()) # ¡HOLA MUNDO!
Primero se aplica @mayusculas, y luego @signos_exclamacion.
Cada decorador modifica el resultado del anterior, formando una cadena de transformaciones.
🟥 Decoradores con argumentos propios
A veces el decorador en sí necesita recibir parámetros. En estos casos, se define una función externa que recibe los parámetros y devuelve el decorador real.
def repetir(n):
"""Ejecuta la función decorada n veces."""
def decorador(funcion):
def envoltura(*args, **kwargs):
for i in range(n):
print(f"Ejecución {i + 1} de {n}")
funcion(*args, **kwargs)
return envoltura
return decorador
@repetir(3)
def saludo():
print("¡Hola!")
saludo()
# Ejecución 1 de 3
# ¡Hola!
# Ejecución 2 de 3
# ¡Hola!
# Ejecución 3 de 3
# ¡Hola!
repetir(3)genera un decorador configurado conn = 3.- Este decorador devuelve la función
envoltura(), que llama tres veces a la función original.
🟪 Uso de functools.wraps
Al aplicar decoradores, Python reemplaza internamente la función original por la función envoltura().
Esto hace que se pierda información como el nombre, el docstring o las anotaciones.
Para conservar esos metadatos, se utiliza el decorador @wraps del módulo functools.
from functools import wraps
def decorador(funcion):
@wraps(funcion)
def envoltura(*args, **kwargs):
print("Ejecutando:", funcion.__name__)
return funcion(*args, **kwargs)
return envoltura
@decorador
def saludar():
"""Muestra un saludo por pantalla."""
print("Hola a todos")
saludar()
print(saludar.__name__)
print(saludar.__doc__)
# Ejecutando: saludar
# Hola a todos
# saludar
# Muestra un saludo por pantalla.
El decorador @wraps(funcion) copia el nombre, docstring y otras propiedades de la función original a la versión decorada.
Esto es una buena práctica recomendada siempre que se definan decoradores personalizados.
🟫 Casos de uso comunes
Los decoradores se utilizan con frecuencia en aplicaciones reales para tareas repetitivas y transversales, como:
- Medición de tiempo de ejecución
import time
def medir_tiempo(funcion):
def envoltura(*args, **kwargs):
inicio = time.time()
resultado = funcion(*args, **kwargs)
fin = time.time()
print(f"Tiempo de ejecución: {fin - inicio:.4f} segundos")
return resultado
return envoltura
@medir_tiempo
def tarea_pesada():
sum([i**2 for i in range(1_000_000)])
tarea_pesada()
- Control de acceso
def requiere_autorizacion(funcion):
def envoltura(usuario):
if usuario != "admin":
print("Acceso denegado.")
return
return funcion(usuario)
return envoltura
@requiere_autorizacion
def panel_administrador(usuario):
print(f"Bienvenido, {usuario}.")
panel_administrador("pepe")
panel_administrador("admin")
- Registro de llamadas
def registrar(funcion):
def envoltura(*args, **kwargs):
print(f"Llamando a {funcion.__name__} con argumentos {args} {kwargs}")
return funcion(*args, **kwargs)
return envoltura
@registrar
def sumar(a, b):
return a + b
sumar(5, 8)
9️⃣ Generadores
Los generadores son un tipo especial de función que permite producir una secuencia de valores de forma progresiva, en lugar de devolverlos todos de una vez.
A diferencia de las funciones tradicionales, que utilizan return, las funciones generadoras emplean la palabra clave yield para devolver un valor y pausar la ejecución hasta la siguiente llamada.
Cada vez que se invoca el generador, este retoma su ejecución desde el punto en que se detuvo, recordando el estado de sus variables internas.
Los generadores tienen múltiples ventajas...
- Eficiencia en memoria.
- Ejecución diferida (lazy evaluation).
- Código más simple que clases iteradoras.
- Apto para flujos infinitos o muy grandes.
🟩 Concepto general
Un generador se define igual que una función normal, pero incluye al menos una instrucción yield.
Cuando se llama a la función, no se ejecuta de inmediato, sino que devuelve un objeto generador, que puede recorrerse con un bucle o con la función next().
def contar_hasta(n):
"""Generador que produce números del 1 al n."""
contador = 1
while contador <= n:
yield contador
contador += 1
for numero in contar_hasta(5):
print(numero)
# 1
# 2
# 3
# 4
# 5
- Cada vez que se ejecuta
yield contador, la función devuelve ese valor y se detiene temporalmente. - Cuando el bucle
forsolicita el siguiente valor, la ejecución se reanuda desde la línea siguiente alyield. - El proceso continúa hasta que se alcanza el final de la función.
🟦 Diferencias entre return y yield
| Característica | return | yield |
|---|---|---|
| Finaliza la función | Sí, termina la ejecución | No, la pausa temporalmente |
| Devuelve | Un único valor | Múltiples valores, uno a uno |
| Guarda el estado interno | No | Sí |
| Se utiliza con | Funciones tradicionales | Generadores |
| Tipo de resultado | Valor concreto | Objeto generador |
Ejemplo comparativo:
def con_return(n):
resultado = []
for i in range(1, n + 1):
resultado.append(i)
return resultado
def con_yield(n):
for i in range(1, n + 1):
yield i
print(con_return(5))
print(list(con_yield(5)))
- La función
con_return()devuelve toda la lista completa[1, 2, 3, 4, 5], ocupando memoria proporcional al tamaño. - En cambio,
con_yield()genera cada número bajo demanda, lo que resulta mucho más eficiente cuando se trabaja con grandes volúmenes de datos.
🟨 Uso de next() con generadores
Los generadores son objetos iterables, por lo que pueden recorrerse manualmente con la función next().
Cada llamada a next() produce el siguiente valor.
Cuando no quedan más, se lanza la excepción StopIteration.
g = contar_hasta(3)
print(next(g)) # 1
print(next(g)) # 2
print(next(g)) # 3
# print(next(g)) -> StopIteration
Cada llamada a next(g) reanuda la ejecución del generador hasta el siguiente yield.
Al completarse, se lanza la excepción StopIteration para indicar que ya no hay más valores.
🟧 Generadores por comprensión
Además de las funciones con yield, Python ofrece generadores por comprensión, similares a las comprensiones de listas, pero con paréntesis en lugar de corchetes.
Estos generadores son aún más compactos y se utilizan para construir iteradores ligeros.
cuadrados = (x ** 2 for x in range(5))
print(cuadrados)
for c in cuadrados:
print(c)
(x ** 2 for x in range(5))crea un generador que calcula los cuadrados de los números del 0 al 4.- No genera una lista en memoria, sino que produce los valores uno a uno cuando se solicitan.
- Este enfoque es muy eficiente en operaciones de cálculo o procesamiento de datos secuenciales.
🟥 Uso de yield from
La instrucción yield from permite delegar parte de la generación a otro iterable o generador, simplificando la sintaxis.
def generador_principal():
yield from range(3)
yield from ["a", "b", "c"]
for valor in generador_principal():
print(valor)
# 0
# 1
# 2
# a
# b
# c
yield from delega la producción de valores a otros iterables, evitando escribir bucles adicionales.
🟪 Generadores avanzados send() y close()
Además de producir valores, los generadores pueden recibir información externa y finalizar de forma controlada.
Estos mecanismos los convierten en pequeñas corutinas.
send(valor): enviar datos al generador
El método send() permite enviar un valor al punto donde el generador está pausado, sustituyendo el valor que devolvería yield.
def cuenta_atras(tiempo):
"""Generador que hace una cuenta atrás y permite modificar el tiempo."""
restante = tiempo
while restante > 0:
ajuste = yield restante # yield devuelve el tiempo actual
if ajuste is not None: # Si se usa send(), el valor llega aquí
restante += ajuste # buff (+) o debuff (-)
restante -= 1 # avanza un segundo (simulado)
g = cuenta_atras(5)
print(next(g)) # 5
print(next(g)) # 4
print(g.send(+3)) # buff: ahora queda 6
print(next(g)) # 5
print(g.send(-2)) # debuff: ahora queda 2
print(next(g)) # 1
print(next(g)) # 0 → se detendrá después
- El generador empieza devolviendo el tiempo inicial y se detiene en la instrucción yield.
- Cada llamada a
next()continúa la cuenta atrás, reduciendo el contador en 1. - Cuando se usa ´, ese valor sustituye al resultado del
yieldy se recibe dentro del generador como ajuste. - Si ajuste es positivo, aumenta el tiempo restante (buff); si es negativo, lo reduce (debuff).
- La cuenta atrás continúa hasta que restante llega a 0, momento en el que el generador finaliza.
close(): cierre controlado del generador
El método close() fuerza al generador a terminar inmediatamente.
Provoca internamente la excepción GeneratorExit, que puede usarse para limpieza.
def generador():
try:
while True:
yield "activo"
except GeneratorExit:
print("Cerrando generador...")
g = generador()
print(next(g)) # activo
g.close() # Cierra el generador
🟫 Ejemplo integrado
def contador():
print("Iniciando generador...")
n = 0
try:
while True:
recibido = yield n
if recibido == "reset":
n = 0
elif isinstance(recibido, int):
n += recibido
except GeneratorExit:
print("Generador cerrado correctamente.")
g = contador()
print(next(g)) # 0
print(g.send(3)) # 3
print(g.send(10)) # 13
print(g.send("reset"))# 0
g.close() # Cierra generador
📊 Tabla resumen
| Concepto | Descripción | Ejemplo |
|---|---|---|
| Función de orden superior | Recibe o devuelve otra función | map(funcion, lista) |
| Función como dato | Puede almacenarse en variables o estructuras | operaciones = [sumar, restar] |
map() | Aplica una función a todos los elementos | map(lambda x: x*2, lista) |
filter() | Filtra elementos según una condición | filter(lambda x:x>0, lista) |
reduce() | Acumula valores de una secuencia | reduce(lambda x,y:x+y, lista) |
| Closure | Función interna que conserva el estado de variables externas | contador() |
| Decorador | Función que recibe y devuelve otra función | @decorador |
| Función interna | Añade nueva lógica a la función original | def envoltura(): |
| Decoradores anidados | Varios decoradores aplicados a la vez | @a encima de @b |
| Decoradores con argumentos | Configurables mediante funciones externas | @repetir(3) |
functools.wraps | Conserva metadatos de la función original | @wraps(funcion) |
| Generador | Función que devuelve una secuencia de valores | yield valor |
| Objeto generador | Resultado de llamar a una función con yield | g = generador() |
yield | Devuelve un valor y pausa la ejecución | dentro de un bucle |
next() | Obtiene el siguiente valor del generador | next(g) |
| Generador por comprensión | Sintaxis abreviada con paréntesis | (x**2 for x in range(5)) |
yield from | Delegación de otro iterable | yield from lista |
send() | Envía un valor al generador y reanuda su ejecución | g.send(valor) |
close() | Finaliza el generador de forma controlada | g.close() |
Las funciones de orden superior son una herramienta fundamental en Python para escribir código flexible, expresivo y reutilizable. Permiten tratar las funciones como valores, lo que amplía enormemente las posibilidades del lenguaje.
Los generadores son una extensión natural del concepto de función. Permiten trabajar con secuencias grandes o infinitas de manera eficiente, conservando la simplicidad y expresividad característica de Python.