Funciones avanzadas

Objetivos del módulo

Al completar este módulo serás capaz de:

  • Usar funciones lambda y map/filter para transformaciones simples
  • Implementar generadores para manejo eficiente de memoria
  • Crear y aplicar decoradores cuando realmente aporten claridad
  • Reconocer técnicas menos frecuentes como reduce y 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, filter y 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))  # 25

Sintaxis

1lambda argumentos: expresión

Casos 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)]
¿Cuándo usar lambda?

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)  # 115

Comparació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]
Preferencia en Python

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_required para proteger páginas
  • @cache para guardar resultados y no recalcular
  • @timer para 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ón

Decorador 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 segundos

Decorador 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, Ana

Preservar 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)  # 2

Generadores

Imagina una imprenta. Si te piden un millón de folletos, tienes dos opciones:

  1. Imprimir todo de golpe: ocupas un almacén entero con papel, y si al final solo necesitaban 100, has desperdiciado recursos
  2. 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))  # 2

Expresiones 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, 16

Generadores 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, 6

Funciones 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))  # 15

Closures: 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
Consejo práctico

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

Ejercicio 1: Transformación de precios

Dada una lista de precios en texto, conviértela a números, descarta los valores vacíos y devuelve una lista con IVA aplicado.

1precios = ["10.5", " 22 ", "", "7.99", "15"]
2
3limpios = filter(lambda texto: texto.strip() != "", precios)
4numeros = map(lambda texto: float(texto.strip()), limpios)
5con_iva = list(map(lambda precio: round(precio * 1.21, 2), numeros))
6
7print(con_iva)  # [12.71, 26.62, 9.67, 18.15]
Ejercicio 2: Generador por lotes

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.

 1def en_lotes(datos, tamano):
 2    for inicio in range(0, len(datos), tamano):
 3        yield datos[inicio:inicio + tamano]
 4
 5numeros = list(range(1, 11))
 6for lote in en_lotes(numeros, 3):
 7    print(lote)
 8
 9# [1, 2, 3]
10# [4, 5, 6]
11# [7, 8, 9]
12# [10]
Ejercicio 3: Reto opcional - Decorador de caché

Implementa un decorador que guarde en caché los resultados de una función para evitar recalcular.

 1from functools import wraps
 2
 3def cache(func):
 4    resultados = {}
 5    
 6    @wraps(func)
 7    def wrapper(*args):
 8        if args not in resultados:
 9            resultados[args] = func(*args)
10        return resultados[args]
11    
12    return wrapper
13
14@cache
15def fibonacci(n):
16    if n < 2:
17        return n
18    return fibonacci(n - 1) + fibonacci(n - 2)
19
20# Sin caché sería muy lento para n grande
21print(fibonacci(35))  # 9227465

Quiz

🎮 Quiz: Funciones avanzadas

0 / 0
Cargando preguntas...

Anterior: Funciones básicas Siguiente: POO