Comprehensions y Generators
Domina el arte de la iteración eficiente y elegante en Python
En la programación Python, la eficiencia y la elegancia van de la mano. Este capítulo explora las herramientas más poderosas del lenguaje para trabajar con colecciones de datos: comprehensions y generators.
Estas técnicas nos permiten escribir código que no solo es más legible y conciso, sino también más eficiente en términos de memoria y tiempo de ejecución. Aprenderemos a transformar bucles verbosos en expresiones elegantes de una sola línea.
Las Funciones map(), zip() y filter()
Antes de sumergirnos en las comprehensions, es fundamental entender las funciones built-in que Python proporciona para manipular colecciones. Estas funciones retornan iteradores, lo que significa que no consumen memoria innecesariamente.
La Función map()
La función map() aplica una función a cada elemento de uno o más iterables, retornando un iterador con los resultados.
# map() aplica una función a cada elemento >>> list(map(lambda x: x**2, range(5))) [0, 1, 4, 9, 16] # Con múltiples iterables >>> list(map(lambda a, b: a + b, [1, 2, 3], [10, 20, 30])) [11, 22, 33] # map() se detiene con el iterable más corto >>> list(map(lambda *a: a, range(3), "abc", range(4, 7))) [(0, 'a', 4), (1, 'b', 5), (2, 'c', 6)]
La Función zip()
La función zip() combina elementos de múltiples iterables en tuplas, creando un iterador de pares (o n-tuplas).
# Combinar dos listas grades = [18, 23, 30, 27] avgs = [22, 21, 29, 24] >>> list(zip(avgs, grades)) [(22, 18), (21, 23), (29, 30), (24, 27)] # Crear diccionarios fácilmente students = ["Sophie", "Alex", "Charlie"] scores = ["A", "C", "B"] >>> dict(zip(students, scores)) {'Sophie': 'A', 'Alex': 'C', 'Charlie': 'B'} # Con strict=True (Python 3.10+) detecta longitudes diferentes >>> dict(zip(students, ["A", "B"], strict=True)) ValueError: zip() argument 2 is shorter than argument 1
La Función filter()
La función filter() crea un iterador con los elementos que satisfacen una condición (predicado).
# Filtrar valores falsy test = [2, 5, 8, 0, 0, 1, 0] >>> list(filter(None, test)) [2, 5, 8, 1] # Filtrar con condición personalizada >>> list(filter(lambda x: x > 4, test)) [5, 8] # Filtrar números pares numbers = range(10) >>> list(filter(lambda n: n % 2 == 0, numbers)) [0, 2, 4, 6, 8]
map(), zip() y filter() retornan iteradores, no listas. Esto significa que no cargan todos los elementos en memoria hasta que los necesitas.
List Comprehensions
Las list comprehensions son una notación concisa para realizar operaciones sobre cada elemento de una colección y/o seleccionar elementos que satisfagan una condición. Son una de las características más elegantes de Python.
Sintaxis Básica
squares = [] for n in range(10): squares.append(n**2) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
squares = [n**2 for n in range(10)] # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Comprehensions con Filtrado
Podemos agregar condiciones para filtrar elementos durante la creación de la lista.
# Usando map y filter (verbose) sq1 = list( map(lambda n: n**2, filter(lambda n: not n % 2, range(10))) ) # Usando list comprehension (elegante) sq2 = [n**2 for n in range(10) if not n % 2] print(sq1 == sq2) # True print(sq2) # [0, 4, 16, 36, 64]
Comprehensions Anidadas
Las comprehensions pueden tener múltiples bucles for anidados, útiles para trabajar con matrices o generar combinaciones.
# Generar pares sin duplicados items = "ABCD" pairs = [ (items[a], items[b]) for a in range(len(items)) for b in range(a, len(items)) ] print(pairs) # [('A','A'), ('A','B'), ('A','C'), ('A','D'), # ('B','B'), ('B','C'), ('B','D'), # ('C','C'), ('C','D'), ('D','D')]
Ejemplo Práctico: Tripletas Pitagóricas
Una tripleta pitagórica es una tupla (a, b, c) de enteros positivos que satisface a² + b² = c².
from math import sqrt mx = 10 # Usando el operador walrus := para evitar cálculos duplicados triples = [ (a, b, int(c)) for a in range(1, mx) for b in range(a, mx) if (c := sqrt(a**2 + b**2)).is_integer() ] print(triples) # [(3, 4, 5), (6, 8, 10)]
:= (Python 3.8+) permite asignar valores dentro de expresiones, evitando cálculos duplicados y haciendo el código más eficiente.
Dictionary y Set Comprehensions
Python también soporta comprehensions para crear diccionarios y sets, usando una sintaxis similar a las list comprehensions.
Dictionary Comprehensions
from string import ascii_lowercase # Crear un mapeo letra -> posición lettermap = {c: k for k, c in enumerate(ascii_lowercase, 1)} print(lettermap) # {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, ...} # Intercambiar mayúsculas/minúsculas word = "Hello" swaps = {c: c.swapcase() for c in word} print(swaps) # {'H': 'h', 'e': 'E', 'l': 'L', 'o': 'O'} # Nota: duplicados se sobrescriben (solo una 'l')
Set Comprehensions
word = "Hello" # Set comprehension - elimina duplicados automáticamente letters1 = {c for c in word} # Equivalente usando set() letters2 = set(c for c in word) print(letters1) # {'H', 'o', 'e', 'l'} print(letters1 == letters2) # True
Generator Functions
Los generators son funciones que permiten iterar sobre una secuencia de valores sin cargarlos todos en memoria. En lugar de return, usan yield para producir valores uno a uno.
Yield vs Return
def get_squares(n): return [x**2 for x in range(n)] # Crea TODA la lista en memoria
def get_squares_gen(n): for x in range(n): yield x**2 # Genera valores uno a uno
Cómo Funcionan los Generators
def get_squares_gen(n): for x in range(n): yield x**2 squares = get_squares_gen(4) # Crea objeto generator print(squares) #print(next(squares)) # 0 print(next(squares)) # 1 print(next(squares)) # 4 print(next(squares)) # 9 print(next(squares)) # StopIteration exception!
Ejemplo: Progresión Geométrica
def geometric_progression(a, q): """Genera términos de la progresión a, aq, aq², aq³, ...""" k = 0 while True: result = a * q**k if result <= 100000: yield result else: return # Termina el generator k += 1 for n in geometric_progression(2, 5): print(n)
10
50
250
1250
6250
31250
La Expresión yield from
La expresión yield from permite delegar la generación de valores a un sub-iterador.
# Sin yield from def print_squares_v1(start, end): for n in range(start, end): yield n**2 # Con yield from - más elegante def print_squares_v2(start, end): yield from (n**2 for n in range(start, end)) for n in print_squares_v2(2, 5): print(n) # 4, 9, 16
Generator Expressions
Las generator expressions son como list comprehensions, pero usan paréntesis en lugar de corchetes y retornan un generador en lugar de una lista.
# List comprehension - carga TODO en memoria cubes_list = [k**3 for k in range(10)] print(type(cubes_list)) ## Generator expression - genera bajo demanda cubes_gen = (k**3 for k in range(10)) print(type(cubes_gen)) # # El generator se puede convertir a lista print(list(cubes_gen)) # [0, 1, 8, 27, 64, ...] print(list(cubes_gen)) # [] - ya está agotado!
Impacto en Memoria: Un Ejemplo Crítico
# ⚠️ CUIDADO con los paréntesis! # Esto FALLA - crea una lista gigante primero s1 = sum([n**2 for n in range(10**10)]) # Killed! # Esto FUNCIONA - usa generator s2 = sum(n**2 for n in range(10**10)) # OK! print(s2) # 333333333283333333335000000000
[...] y (...) puede ser la diferencia entre un programa que funciona y uno que agota toda tu memoria. Siempre prefiere generators cuando solo necesitas iterar una vez.
Beneficios de los Generators
Consideraciones de Performance
Elegir entre bucles, comprehensions y generators tiene implicaciones tanto en legibilidad como en rendimiento.
Comparación de Velocidad
from time import time mx = 5000 # Método 1: Bucle for tradicional t = time() floop = [] for a in range(1, mx): for b in range(a, mx): floop.append(divmod(a, b)) print(f"for loop: {time() - t:.4f} s") # Método 2: List comprehension t = time() compr = [divmod(a, b) for a in range(1, mx) for b in range(a, mx)] print(f"list comprehension: {time() - t:.4f} s") # Método 3: Generator expression t = time() gener = list(divmod(a, b) for a in range(1, mx) for b in range(a, mx)) print(f"generator expression: {time() - t:.4f} s")
list comprehension: 1.6882 s
generator expression: 1.6525 s
| Método | Velocidad | Memoria | Legibilidad |
|---|---|---|---|
| for loop | ~100% | Alta | ⭐⭐⭐⭐⭐ |
| List comprehension | ~71% | Alta | ⭐⭐⭐⭐ |
| Generator expression | ~69% | Baja | ⭐⭐⭐⭐ |
| map() | ~31%* | Baja | ⭐⭐⭐ |
* El rendimiento de map() depende del contexto y puede variar significativamente.
Cuándo Usar Cada Opción
Caso Práctico: Secuencia de Fibonacci
Veamos cómo evoluciona una implementación de la secuencia de Fibonacci desde un enfoque básico hasta uno elegante usando generators.
Versión 1: Enfoque Básico
def fibonacci(N): """Retorna todos los números Fibonacci hasta N.""" result = [0] next_n = 1 while next_n <= N: result.append(next_n) next_n = sum(result[-2:]) return result print(fibonacci(50)) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Versión 2: Generator Function
def fibonacci(N): """Genera números Fibonacci hasta N.""" yield 0 if N == 0: return a, b = 0, 1 while b <= N: yield b a, b = b, a + b print(list(fibonacci(50))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Versión 3: La Más Elegante
def fibonacci(N): """Genera números Fibonacci hasta N.""" a, b = 0, 1 while a <= N: yield a a, b = b, a + b # Uso como iterador for num in fibonacci(50): print(num, end=" ") # 0 1 1 2 3 5 8 13 21 34
Resumen del Capítulo
01 // map(), zip(), filter()
Funciones built-in que retornan iteradores para transformar, combinar y filtrar colecciones de manera eficiente.
02 // List Comprehensions
Sintaxis concisa [expr for x in iterable if cond] para crear listas de forma elegante y expresiva.
03 // Dict/Set Comprehensions
Extensión del concepto para crear diccionarios {k:v for...} y sets {x for...} con la misma elegancia.
04 // Generator Functions
Funciones con yield que producen valores bajo demanda, ideales para grandes volúmenes de datos.
05 // Generator Expressions
Como comprehensions pero con paréntesis (expr for...), perfectas para pipelines de procesamiento.
06 // Performance
Comprehensions y map() son más rápidos que for loops, pero la legibilidad siempre debe ser prioritaria.