Funciones avanzadas
Al completar este módulo serás capaz de:
- Usar funciones lambda y
map/filterpara transformaciones simples - Implementar generadores para manejo eficiente de memoria
- Crear y aplicar decoradores cuando realmente aporten claridad
- Reconocer técnicas menos frecuentes como
reducey los closures
El siguiente nivel
Ya sabes crear funciones básicas: definir parámetros, devolver valores y organizar tu código en bloques reutilizables. El siguiente paso no es aprender “trucos”, sino entender qué herramientas merece la pena usar a menudo y cuáles conviene conocer sin abusar de ellas.
En este módulo vamos a separar esas dos capas:
- Uso frecuente:
lambda,map,filtery generadores - Uso situacional: decoradores
- Ampliación:
reduce, decoradores de clase y closures
La idea es que salgas de aquí sabiendo qué usar primero, qué dejar para más adelante y qué técnicas merecen una segunda lectura cuando ya tengas más práctica.
Funciones lambda
Imagina que estás en una reunión y necesitas apuntar algo rápido. No vas a sacar un cuaderno, buscar un bolígrafo y escribir una nota formal. Coges un Post-it, garabateas lo esencial, y listo.
Las funciones lambda son exactamente eso: las notas Post-it de Python. Funciones pequeñas, de usar y tirar, para cuando definir una función completa con def sería matar moscas a cañonazos.
1# Función normal (el cuaderno formal)
2def cuadrado(x):
3 return x ** 2
4
5# Equivalente como lambda (el Post-it)
6cuadrado = lambda x: x ** 2
7
8print(cuadrado(5)) # 25Sintaxis
1lambda argumentos: expresiónCasos de uso comunes
1# Ordenar con criterio personalizado
2personas = [
3 {"nombre": "Ana", "edad": 25},
4 {"nombre": "Luis", "edad": 30},
5 {"nombre": "María", "edad": 22}
6]
7
8# Ordenar por edad
9ordenados = sorted(personas, key=lambda p: p["edad"])
10print(ordenados)
11
12# Ordenar por nombre
13ordenados = sorted(personas, key=lambda p: p["nombre"])
14
15# Ordenar lista de tuplas por segundo elemento
16puntos = [(1, 5), (3, 2), (2, 8)]
17ordenados = sorted(puntos, key=lambda p: p[1])
18print(ordenados) # [(3, 2), (1, 5), (2, 8)]Usa lambda para funciones simples de una línea, especialmente como argumento de otras funciones. Si la lógica es compleja, usa una función normal con def.
Programación funcional
Imagina una cadena de montaje en una fábrica. Las piezas entran por un lado, pasan por diferentes estaciones que las transforman o filtran, y salen productos terminados por el otro.
La programación funcional aplica esta misma filosofía al código: los datos fluyen a través de funciones que los transforman, filtran o combinan. Sin efectos secundarios, sin modificar los datos originales.
graph LR
A["[1,2,3,4,5]"] --> B["map(x²)"]
B --> C["[1,4,9,16,25]"]
C --> D["filter(>5)"]
D --> E["[9,16,25]"]
E --> F["reduce(+)"]
F --> G["50"]Python ofrece tres herramientas clásicas para esto: map, filter y reduce.
map(): La máquina transformadora
map() es como una máquina que aplica la misma operación a cada pieza que pasa. Entra una lista, sale otra lista del mismo tamaño pero con cada elemento transformado.
1numeros = [1, 2, 3, 4, 5]
2
3# Calcular cuadrados
4cuadrados = list(map(lambda x: x**2, numeros))
5print(cuadrados) # [1, 4, 9, 16, 25]
6
7# Convertir a strings
8textos = list(map(str, numeros))
9print(textos) # ['1', '2', '3', '4', '5']
10
11# Con múltiples iterables
12a = [1, 2, 3]
13b = [10, 20, 30]
14sumas = list(map(lambda x, y: x + y, a, b))
15print(sumas) # [11, 22, 33]filter(): El control de calidad
filter() es el inspector de calidad de la cadena de montaje. Examina cada pieza y decide: ¿cumple los requisitos? Si sí, pasa. Si no, se descarta. La lista resultante puede ser más pequeña que la original.
1numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3# Filtrar pares
4pares = list(filter(lambda x: x % 2 == 0, numeros))
5print(pares) # [2, 4, 6, 8, 10]
6
7# Filtrar mayores que 5
8mayores = list(filter(lambda x: x > 5, numeros))
9print(mayores) # [6, 7, 8, 9, 10]
10
11# Filtrar strings no vacíos
12palabras = ["hola", "", "mundo", "", "python"]
13no_vacias = list(filter(None, palabras)) # None filtra valores "falsy"
14print(no_vacias) # ['hola', 'mundo', 'python']reduce(): La prensa compactadora (uso menos frecuente)
reduce() es la máquina que compacta todo en uno solo. Toma el primer elemento, lo combina con el segundo, el resultado con el tercero, y así sucesivamente hasta quedarse con un único valor final.
1from functools import reduce
2
3numeros = [1, 2, 3, 4, 5]
4
5# Suma acumulativa
6suma = reduce(lambda acc, x: acc + x, numeros)
7print(suma) # 15
8
9# Producto
10producto = reduce(lambda acc, x: acc * x, numeros)
11print(producto) # 120
12
13# Encontrar el máximo
14maximo = reduce(lambda a, b: a if a > b else b, numeros)
15print(maximo) # 5
16
17# Con valor inicial
18suma_desde_100 = reduce(lambda acc, x: acc + x, numeros, 100)
19print(suma_desde_100) # 115Comparación: funcional vs comprensión
1numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3# Funcional
4cuadrados_pares = list(map(lambda x: x**2, filter(lambda x: x % 2 == 0, numeros)))
5
6# Comprensión de lista (más legible en Python)
7cuadrados_pares = [x**2 for x in numeros if x % 2 == 0]
8
9print(cuadrados_pares) # [4, 16, 36, 64, 100]En Python, las comprensiones de lista suelen ser más legibles que map/filter. Usa programación funcional cuando tenga sentido o cuando trabajes con funciones ya existentes.
Decoradores
Imagina que tienes un regalo ya empaquetado. Ahora quieres añadirle un lazo bonito, papel brillante y una tarjeta. El regalo sigue siendo el mismo por dentro, pero ahora tiene funcionalidades extra: es más bonito, tiene un mensaje, quizás hasta suena al agitarlo.
Los decoradores hacen exactamente eso con las funciones: las “envuelven” añadiendo comportamiento extra sin modificar su código interno.
graph LR
A["Llamada a función"] --> B["Decorador: ANTES"]
B --> C["Función original"]
C --> D["Decorador: DESPUÉS"]
D --> E["Resultado"]¿Dónde los verás en el mundo real?
@app.route("/")en Flask para definir rutas web@login_requiredpara proteger páginas@cachepara guardar resultados y no recalcular@timerpara medir cuánto tarda una función
Sintaxis básica
1def mi_decorador(func):
2 def wrapper(*args, **kwargs):
3 print("Antes de la función")
4 resultado = func(*args, **kwargs)
5 print("Después de la función")
6 return resultado
7 return wrapper
8
9@mi_decorador
10def saludar(nombre):
11 print(f"¡Hola, {nombre}!")
12
13saludar("Ana")
14# Salida:
15# Antes de la función
16# ¡Hola, Ana!
17# Después de la funciónDecorador de medición de tiempo
1import time
2
3def medir_tiempo(func):
4 def wrapper(*args, **kwargs):
5 inicio = time.time()
6 resultado = func(*args, **kwargs)
7 fin = time.time()
8 print(f"{func.__name__} tardó {fin - inicio:.4f} segundos")
9 return resultado
10 return wrapper
11
12@medir_tiempo
13def proceso_lento():
14 time.sleep(1)
15 return "Completado"
16
17resultado = proceso_lento()
18# proceso_lento tardó 1.0012 segundosDecorador con parámetros
1def repetir(veces):
2 def decorador(func):
3 def wrapper(*args, **kwargs):
4 for _ in range(veces):
5 resultado = func(*args, **kwargs)
6 return resultado
7 return wrapper
8 return decorador
9
10@repetir(3)
11def saludar(nombre):
12 print(f"Hola, {nombre}")
13
14saludar("Ana")
15# Hola, Ana
16# Hola, Ana
17# Hola, AnaPreservar metadatos con functools.wraps
1from functools import wraps
2
3def mi_decorador(func):
4 @wraps(func) # Preserva __name__, __doc__, etc.
5 def wrapper(*args, **kwargs):
6 return func(*args, **kwargs)
7 return wrapper
8
9@mi_decorador
10def mi_funcion():
11 """Esta es la documentación."""
12 pass
13
14print(mi_funcion.__name__) # mi_funcion (sin @wraps sería 'wrapper')
15print(mi_funcion.__doc__) # Esta es la documentación.Decoradores de clase (ampliación)
1class ContadorLlamadas:
2 def __init__(self, func):
3 self.func = func
4 self.llamadas = 0
5
6 def __call__(self, *args, **kwargs):
7 self.llamadas += 1
8 print(f"Llamada #{self.llamadas}")
9 return self.func(*args, **kwargs)
10
11@ContadorLlamadas
12def saludar():
13 print("Hola")
14
15saludar() # Llamada #1 / Hola
16saludar() # Llamada #2 / Hola
17print(saludar.llamadas) # 2Generadores
Imagina una imprenta. Si te piden un millón de folletos, tienes dos opciones:
- Imprimir todo de golpe: ocupas un almacén entero con papel, y si al final solo necesitaban 100, has desperdiciado recursos
- Imprimir bajo demanda: solo produces un folleto cuando alguien lo pide
Los generadores son la opción 2. En lugar de crear una lista gigante en memoria y devolvértela entera, producen valores uno a uno, solo cuando los pides. Son “perezosos” en el mejor sentido: no trabajan más de lo necesario.
| Lista tradicional | Generador |
|---|---|
| Crea todo en memoria de golpe | Produce valores uno a uno |
| Ocupa memoria proporcional al tamaño | Ocupa memoria constante (mínima) |
| Puedes acceder a cualquier elemento | Solo puedes avanzar, no retroceder |
| Ideal para datos pequeños | Ideal para datos grandes o infinitos |
Funciones generadoras con yield
La palabra mágica es yield. A diferencia de return (que termina la función), yield pausa la función y devuelve un valor. La próxima vez que pidas un valor, continúa donde lo dejó.
1def cuenta_hasta(n):
2 i = 1
3 while i <= n:
4 yield i
5 i += 1
6
7# Usar el generador
8for num in cuenta_hasta(5):
9 print(num) # 1, 2, 3, 4, 5
10
11# El generador es perezoso (lazy)
12gen = cuenta_hasta(1000000)
13print(next(gen)) # 1
14print(next(gen)) # 2Expresiones generadoras
1# Lista (ocupa memoria)
2cuadrados_lista = [x**2 for x in range(1000000)]
3
4# Generador (no ocupa memoria hasta que se usa)
5cuadrados_gen = (x**2 for x in range(1000000))
6
7# Iterar sobre el generador
8for i, cuadrado in enumerate(cuadrados_gen):
9 if i >= 5:
10 break
11 print(cuadrado) # 0, 1, 4, 9, 16Generadores para archivos grandes
1def leer_en_chunks(archivo, tamaño=1024):
2 """Lee un archivo en fragmentos para ahorrar memoria."""
3 with open(archivo, 'r') as f:
4 while True:
5 chunk = f.read(tamaño)
6 if not chunk:
7 break
8 yield chunk
9
10# Procesar archivo grande sin cargarlo todo en memoria
11for chunk in leer_en_chunks('archivo_grande.txt'):
12 procesar(chunk)yield from
1def generador_anidado():
2 yield from [1, 2, 3]
3 yield from [4, 5, 6]
4
5for num in generador_anidado():
6 print(num) # 1, 2, 3, 4, 5, 6Funciones de orden superior y Closures (ampliación)
Hasta ahora, hemos tratado las funciones como herramientas: las defines, las llamas, devuelven algo. Pero en Python, las funciones son ciudadanos de primera clase. Esto significa que puedes:
- Guardarlas en variables
- Pasarlas como argumentos a otras funciones
- Devolverlas como resultado de otras funciones
Esto último es lo que llamamos funciones de orden superior: funciones que crean y devuelven otras funciones.
1def crear_multiplicador(factor):
2 def multiplicar(x):
3 return x * factor
4 return multiplicar
5
6doble = crear_multiplicador(2)
7triple = crear_multiplicador(3)
8
9print(doble(5)) # 10
10print(triple(5)) # 15Closures: Funciones con memoria
Aquí viene lo interesante. En el ejemplo anterior, cuando llamamos a doble(5), la función multiplicar necesita el valor de factor. Pero crear_multiplicador ya terminó de ejecutarse… ¿de dónde sale factor?
La respuesta son los closures. Una función interna “recuerda” las variables del ámbito donde fue creada, incluso después de que ese ámbito haya terminado.
Piensa en un empleado que recibe instrucciones el primer día (“multiplica todo por 2”) y las recuerda para siempre, aunque el jefe que se las dio ya no esté.
1def contador():
2 count = 0
3 def incrementar():
4 nonlocal count
5 count += 1
6 return count
7 return incrementar
8
9mi_contador = contador()
10print(mi_contador()) # 1
11print(mi_contador()) # 2
12print(mi_contador()) # 3¿Cuándo usar cada técnica?
| Técnica | Úsala cuando… | Ejemplo típico |
|---|---|---|
| Lambda | Necesitas una función simple y desechable | sorted(lista, key=lambda x: x[“edad”]) |
| map/filter | Transformas o filtras listas de datos | Convertir precios, filtrar usuarios activos |
| reduce | Reduces una lista a un solo valor | Sumar totales, encontrar máximo |
| Decoradores | Quieres añadir comportamiento sin modificar la función | Logging, caché, autenticación, medir tiempo |
| Generadores | Trabajas con datos grandes o infinitos | Leer archivos enormes, streams de datos |
| Closures | Necesitas funciones que “recuerden” valores | Crear funciones personalizadas, contadores |
No uses estas técnicas solo por usarlas. Si un bucle for simple resuelve tu problema de forma clara, úsalo. Estas herramientas brillan en situaciones específicas, no son un reemplazo universal.
Ejercicios prácticos
Dada una lista de precios en texto, conviértela a números, descarta los valores vacíos y devuelve una lista con IVA aplicado.
Crea un generador que reciba una lista y vaya devolviendo lotes de tamaño fijo para procesarla sin cargar todo de golpe en una sola operación.
Implementa un decorador que guarde en caché los resultados de una función para evitar recalcular.