lunes, 19 de enero de 2026

Aprendiendo Python de 0 a experto - Colecciones y Generadores

Comprehensions y Generators en Python
Python // Capítulo 05

Comprehensions y Generators

Domina el arte de la iteración eficiente y elegante en Python

"No es el aumento diario, sino la disminución diaria. Elimina lo no esencial."
— Bruce Lee

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.

MÓDULO_01

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.

Flujo de map()
iterable
——→
map(función)
——→
iterador
map_example.py
# 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).

zip_example.py
# 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).

filter_example.py
# 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]
Las funciones map(), zip() y filter() retornan iteradores, no listas. Esto significa que no cargan todos los elementos en memoria hasta que los necesitas.
MÓDULO_02

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

Anatomía de una List Comprehension
[expresión
for item in iterable
if condición]
Bucle Tradicional
squares = []
for n in range(10):
    squares.append(n**2)

# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
List Comprehension
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.

even_squares.py
# 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.

nested_comprehension.py
# 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².

pythagorean_triples.py
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)]
El operador walrus := (Python 3.8+) permite asignar valores dentro de expresiones, evitando cálculos duplicados y haciendo el código más eficiente.
MÓDULO_03

Dictionary y Set Comprehensions

Python también soporta comprehensions para crear diccionarios y sets, usando una sintaxis similar a las list comprehensions.

Dictionary Comprehensions

Sintaxis Dict Comprehension
{
key: value
for item in iterable
}
dict_comprehension.py
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

set_comprehension.py
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
Tanto los diccionarios como los sets no permiten duplicados. En diccionarios, las claves duplicadas se sobrescriben con el último valor. En sets, los valores duplicados simplemente se ignoran.
MÓDULO_04

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

Diferencia Conceptual
return → Termina la función, retorna todo de una vez
yield → Pausa la función, retorna un valor, continúa después
Función Tradicional
def get_squares(n):
    return [x**2 for x in range(n)]

# Crea TODA la lista en memoria
Generator Function
def get_squares_gen(n):
    for x in range(n):
        yield x**2

# Genera valores uno a uno

Cómo Funcionan los Generators

generator_manual.py
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

geometric_progression.py
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)
python geometric_progression.py
2
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.

yield_from.py
# 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
MÓDULO_05

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.

Sintaxis Comparada
[expr for x in iterable] → Lista
vs
(expr for x in iterable) → Generator
generator_expression.py
# 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

memory_comparison.py
# ⚠️ 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
La diferencia entre [...] 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

MEMORIA
Solo mantiene un elemento en memoria a la vez, ideal para datos masivos
LAZY EVALUATION
Los valores se calculan solo cuando se necesitan, ahorrando cómputo
INFINITOS
Pueden representar secuencias infinitas que serían imposibles con listas
MÓDULO_06

Consideraciones de Performance

Elegir entre bucles, comprehensions y generators tiene implicaciones tanto en legibilidad como en rendimiento.

Comparación de Velocidad

performance.py
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")
python performance.py
for loop: 2.3832 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

FOR LOOP
Lógica compleja, múltiples operaciones por iteración, máxima claridad
COMPREHENSION
Transformaciones simples, necesitas una lista, código conciso
GENERATOR
Datos grandes, iteración única, pipelines de procesamiento
Recuerda el Zen de Python: "La legibilidad cuenta" y "Si la implementación es difícil de explicar, es una mala idea". No sacrifiques claridad por unos microsegundos.
MÓDULO_07

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

fibonacci_v1.py
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

fibonacci_v2.py
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

fibonacci_elegant.py
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
La versión final usa solo 4 líneas de código (5 con docstring) y es más eficiente en memoria porque no almacena toda la secuencia, solo los dos últimos valores necesarios para calcular el siguiente.

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.

Basado en "Learn Python Programming" 4th Edition // Fabrizio Romano & Heinrich Kruger

Publicado por Packt Publishing