Python generators: programación eficiente en memoria

Los generadores de Python son un tipo especial de función en Python. Generan valores paso por paso y permiten así un uso eficiente de la memoria.

¿Qué son exactamente los Python generators?

Los Python generators son funciones especiales que devuelven un iterator en Python. La forma de crear Python generators es similar a la definición de una función normal. La diferencia está en los detalles: en lugar de una sentencia return, los generadores tienen una sentencia yield. Además, las funciones generadoras como los iterators también implementan una función next().

Nota

Los Python generators pertenecen a conceptos avanzados de la programación en Python. Si tus conocimientos ya van más allá de los tutoriales de Python para principiantes, puedes echar un vistazo a estos artículos:

La palabra clave yield

Si ya tienes experiencia con otros lenguajes de programación o con Python, entonces conoces la sentencia return. Se utiliza para pasar los valores calculados por las funciones a la instancia de llamada en el código del programa. Una vez alcanzada la sentencia return de una función, se sale de la función y finaliza su ejecución. Si es necesario, se puede volver a llamar la función.

Con yield es diferente: esta palabra sustituye a return en los Python generators. Si ahora llamas a tu generador, se devolverá el valor que pases a la sentencia yield. Después, sin embargo, no se sale de un Python generator, sino que este simplemente se interrumpe. El estado actual de la función del generador se guarda. Cuando vuelvas a abrir la función de tu generador, saltarás a la posición guardada.

Usos de los generadores de Python

Dado que los generadores de Python funcionan según el principio de “Lazy Evaluation” y solo se evalúan los valores cuando son realmente necesarios, las funciones generadoras son perfectas para trabajar con grandes cantidades de datos.

Una función normal cargaría primero todo el contenido del fichero en una variable y, por tanto, en la memoria. Con grandes cantidades de datos, tu memoria local podría no ser suficiente y el proceso llevaría a un MemoryError. Los generadores facilitan la resolución de estos problemas leyendo el fichero línea por línea. La palabra clave yield devuelve el valor que necesitas en ese momento e interrumpe la ejecución de la función hasta la siguiente llamada a la función, que procesa otra línea del fichero.

Consejo

Muchas aplicaciones web requieren el procesamiento de grandes cantidades de datos. Python también es adecuado para su uso en proyectos web. Con Deploy Now puedes acelerar la creación de tus proyectos web beneficiándote del despliegue y la construcción automáticos a través de GitHub.

Sin embargo, no solo el manejo de grandes cantidades de datos, sino también el trabajo con infinitos se ve enormemente facilitado por los Python generators. Dado que la memoria local es finita, los generadores son la única forma de crear listas infinitas o similares en Python.

Lectura de archivos CSV con Python generators

Como ya se ha mencionado, los generadores son especialmente adecuados para trabajar con grandes cantidades de datos. El siguiente programa permite leer un archivo CSV línea por línea de forma eficiente para la memoria:

import csv
def leer_csv (nombredefichero):
 with open(nombredefichero, 'r') as fichero:
  tmp = csv.reader(fichero)
  for línea in tmp:
   yield línea
for línea in leer_csv ('test.csv'):
 print(línea)
Python

En el ejemplo de código, primero se importa el módulo csv para tener acceso a las funciones de Python para procesar archivos CSV. Luego se puede ver la definición de un generador de Python llamado “leer_csv”, que comienza con la palabra clave “def” como las definiciones de funciones. Después de abrir el archivo, el bucle for en Python recorre el archivo línea por línea. Cada línea se devuelve con la palabra clave “yield”.

Fuera de la función generadora, las líneas devueltas por el Python Generator se imprimen una tras otra en la consola. Para ello se utiliza la función Python print.

Creación de estructuras de datos infinitas con Python Generators

Lógicamente, una estructura de datos infinita no puede almacenarse localmente en el ordenador. Sin embargo, las estructuras de datos infinitas son esenciales para algunas aplicaciones. Las funciones generadoras también ayudan en este caso, ya que procesan todos los elementos uno tras otro y así no inundan la memoria. Este podría ser un ejemplo de una secuencia infinita de números naturales en código Python:

def numeros_naturales():
 n = 0
 while True:
  yield n
  n += 1
for cifra in numeros_naturales():
 print(cifra)
Python

En primer lugar, se define un Python generator llamado “numeros_naturales”, que establece el valor inicial de la variable “n”. A continuación, se inicia un bucle while en Python que se ejecuta sin fin. Con “yield” se devuelve el valor actual de la variable y se interrumpe la ejecución de la función generadora. Si se abre la función por segunda vez, el número previamente emitido se incrementará en 1 y el generador se ejecutará de nuevo hasta que el intérprete encuentre la palabra clave “yield”. En el bucle for, que se encuentra debajo de la función generadora, se devuelven los números generados. Si el programa no se interrumpe manualmente, se ejecutará sin fin.

Abreviatura de los generadores en Python

Con List Comprehensions puedes crear listas de Python en una sola línea de código. Existe una abreviatura similar para los generadores. A continuación se presenta un generador que incrementa con valor 1 los números del 0 al 9. Es similar al generador utilizado para generar la secuencia infinita de números naturales.

def numeros_naturales():
 n = 0
 while n <= 9:
  yield n
  n+=1
Python

Si deseas escribir este generador en una línea de código, utiliza una sentencia for entre paréntesis como en el siguiente ejemplo:

increment_generator = (n + 1 for n in range(10))
Python

Si ahora quieres devolver este generador, obtendrás el siguiente resultado:

<generator object <genexpr> at 0x0000020CC5A2D6C8>

De este modo se muestra en qué lugar de la memoria se encuentra el objeto generador creado. Para acceder al resultado de tu generador, puedes utilizar la función next():

print(next(increment_generator))
print(next(increment_generator))
print(next(increment_generator))
Python

Esta sección de código proporciona el siguiente resultado, donde los números de 0 a 2 se han incrementado cada uno en 1:

1
2
3

Generadores vs. List Comprehensions

La abreviatura de los generadores es muy similar a la de los List Comprehensions. La única diferencia visual radica en los corchetes: mientras que para las Comprehensions se utilizan corchetes, para la creación de generadores de Python se utilizan paréntesis. Pero internamente hay una diferencia mucho más significativa: los requisitos de memoria de los generadores son mucho menores que los de las listas.

import sys
listadeincremento = [n + 1 for n in range(100)]
generadordeincremento = (n + 1 for n in range(100))
print(sys.getsizeof(listadeincremento))
print(sys.getsizeof(generadordeincremento))
Python

El programa anterior calcula las necesidades de memoria de la lista y del generador equivalente:

912
120

Mientras que la lista requiere 912 bytes, el generador solo necesita 120. Esta diferencia va aumentando conforme incrementa la cantidad de datos que hay que procesar.