Generadores en Python: Conceptos y Uso Avanzado

Los generadores en Python ofrecen una forma potente y eficiente de manejar secuencias de datos, especialmente cuando se trata de conjuntos grandes o infinitos. A diferencia de las listas o tuplas que almacenan todos sus elementos en memoria, los generadores producen valores "sobre la marcha", lo que resulta en un consumo de memoria significativamente menor y una mayor eficiencia en ciertos escenarios. Se manifiestan principalmente como funciones generadoras y expresiones generadoras.

Funciones Generadoras

Una función se convierte en una función generadora si contiene la palabra clave yield. En lugar de devolver un valor y terminar su ejecución, yield pausa la función, devuelve un valor al llamador y guarda su estado interno. La próxima vez que se solicite un valor, la función generadora reanuda su ejecución desde donde se detuvo.


def secuencia_paso_a_paso():
   print("Iniciando la secuencia...")
   yield "Primer elemento"
   print("Continuando después del primer elemento...")
   yield "Segundo elemento"
   print("Finalizando la secuencia.")

# Al llamar a la función, no se ejecuta su cuerpo directamente,
# sino que se devuelve un objeto generador.
mi_generador = secuencia_paso_a_paso()
print(f"Tipo del objeto: {type(mi_generador)}") # Salida: <class>

# Para obtener los valores, se utiliza el método __next__() o la función next().
print("--- Obteniendo elementos ---")
valor1 = next(mi_generador) # Esto ejecuta hasta el primer yield
print(f"Recibido: {valor1}")

valor2 = mi_generador.__next__() # Esto ejecuta hasta el segundo yield
print(f"Recibido: {valor2}")

try:
   mi_generador.__next__() # Intentar obtener un tercer valor levantará StopIteration
except StopIteration:
   print("No hay más elementos en el generador.")
   </class>

La principal ventaja de las funciones generadoras es su capacidad para gestionar grandes volúmenes de datos sin cargar toda la información en la memoria RAM. Esto las hace ideales para procesar archivos grandes, transmisiones de datos o colecciones potencialmente infinitas.


def generador_numerico(limite_superior):
   """Genera números enteros desde 0 hasta limite_superior-1."""
   for numero in range(limite_superior):
       yield numero

# Crear un generador para 1 millón de números (no se crea la lista completa en memoria)
numeros_grandes = generador_numerico(1_000_000)

# Acceder a los primeros elementos
print(f"Primer número: {next(numeros_grandes)}")
print(f"Segundo número: {numeros_grandes.__next__()}")
   

Consideraciones al Re-invocar Funciones Generadoras

Es importante recordar que cada vez que se invoca una función generadora, se crea una nueva instancia de generador. Esto significa que si se llama a la función múltiples veces y se intenta obtener un valor de cada instancia recién creada, siempre se obtendrá el primer valor de cada una.


def generador_simple_contador():
   for x in range(3):
       yield x

# Cada llamada a generador_simple_contador() crea un generador nuevo, independiente de los demás.
print(f"Primera llamada: {next(generador_simple_contador())}") # Imprime 0
print(f"Segunda llamada: {next(generador_simple_contador())}") # Imprime 0 (generador diferente)
print(f"Tercera llamada: {next(generador_simple_contador())}") # Imprime 0 (generador diferente)

# Para iterar sobre el mismo generador, debes asignarlo a una variable.
mi_gen_unico = generador_simple_contador()
print(f"Único generador, primer elemento: {next(mi_gen_unico)}")   # Imprime 0
print(f"Único generador, segundo elemento: {next(mi_gen_unico)}")  # Imprime 1
   

El Método send()

Además de next(), los generadores tianen un método send(valor) que permite no solo avanzar el generador, sino también enviar un valor de vuelta al punto donde se detuvo el último yield. El valor enviado se convierte en el resultado de la expresión yield.


def procesador_interactivo():
   print("Listo para procesar entradas.")
   # El valor enviado se asignará a 'entrada'
   entrada = yield "Esperando datos"
   print(f"Recibí: '{entrada}'")
   resultado = entrada.upper() if entrada else "VACÍO"
   yield resultado # Retornar un resultado basado en la entrada
   print("Proceso finalizado.")

gen_interactivo = procesador_interactivo()

# La primera llamada a send() o next() no tiene un yield previo para recibir un valor.
# Por lo tanto, la primera llamada a send() DEBE ser con None.
primer_mensaje = gen_interactivo.send(None)
print(f"El generador dijo: '{primer_mensaje}'")

# Ahora podemos enviar un valor. Este valor se asignará a 'entrada' en el generador.
respuesta_generador = gen_interactivo.send("Hola mundo")
print(f"El generador respondió: '{respuesta_generador}'")

try:
   gen_interactivo.send("Otro valor") # Intentar enviar más allá del último yield levantará StopIteration
except StopIteration:
   print("El generador ha terminado su ejecución.")
   

yield from para Delegar Iteración

La expresión yield from se utiliza para delegar una parte de la operación de un generador a otro iterador o generador. Esto simplifica el código y mejora la legibilidad cuando un generador necesita consumir elementos de otro iterable.


def generador_principal_con_delegacion():
   print("Iniciando generador principal.")
   yield "Inicio"
   # Delegar la iteración a una lista de números
   yield from [10, 20, 30]
   # Delegar la iteración a un generador de letras
   yield from generar_letras()
   yield "Fin"
   print("Generador principal terminado.")

def generar_letras():
   yield 'A'
   yield 'B'
   yield 'C'

iterador_delegado = generador_principal_con_delegacion()

for elemento in iterador_delegado:
   print(f"Obtenido: {elemento}")
   

Expresiones Generadoras

Similar a las comprensiones de listas (list comprehensions), Python ofrece las expresiones generadoras, que son una forma concisa de crear objetos generadores. Se definen utilizando paréntesis en lugar de corchetes (como en las listas) o llaves (como en los conjuntos o diccionarios).


# Ejemplos de comprensiones (no generadores, solo para contexto):
# Comprensión de lista: crea una lista completa en memoria
cuadrados_lista = [x*x for x in range(5)]
print(f"Lista de cuadrados: {cuadrados_lista}") # [0, 1, 4, 9, 16]

# Comprensión de conjunto: crea un conjunto completo en memoria
letras_unicas = {c for c in "Mississippi"}
print(f"Conjunto de letras: {letras_unicas}") # {'s', 'p', 'i', 'M'}

# Comprensión de diccionario: crea un diccionario completo en memoria
numeros_dobles = {i: i*2 for i in range(3)}
print(f"Diccionario de dobles: {numeros_dobles}") # {0: 0, 1: 2, 2: 4}

# --- Expresión Generadora ---
# Crea un objeto generador que produce valores de forma perezosa (on-the-fly)
cuadrados_generador = (x*x for x in range(5))
print(f"Objeto generador: {cuadrados_generador}") # Salida: <generator object <genexpr> at 0x...>

# Los valores se obtienen uno a uno, solo cuando se necesitan
print(f"Primer cuadrado: {next(cuadrados_generador)}")
print(f"Segundo cuadrado: {next(cuadrados_generador)}")
print(f"Todos los elementos restantes: {list(cuadrados_generador)}") # Consumir el resto
   

Las expresiones generadoras son especialmente útiles cuando se necesita un generador para una iteración simple y no se justifica la creación de una función generadora completa.

Etiquetas: Python generadores Yield iteradores RendimientoPython

Publicado el 6-24 19:52