Rendimiento de Python, generadores y expresiones de generador

En este tutorial, aprenderá cómo crear iteraciones fácilmente usando generadores de Python, en qué se diferencia de los iteradores y las funciones normales, y por qué debería usarlo.

Video: Generadores de Python

Generadores en Python

Hay mucho trabajo en la construcción de un iterador en Python. Tenemos que implementar una clase con un método __iter__()y __next__(), realizar un seguimiento de los estados internos y aumentar StopIterationcuando no hay valores para devolver.

Esto es largo y contradictorio. Generator viene al rescate en tales situaciones.

Los generadores de Python son una forma sencilla de crear iteradores. Todo el trabajo que mencionamos anteriormente es manejado automáticamente por generadores en Python.

Simplemente hablando, un generador es una función que devuelve un objeto (iterador) sobre el que podemos iterar (un valor a la vez).

Crear generadores en Python

Es bastante sencillo crear un generador en Python. Es tan fácil como definir una función normal, pero con una yielddeclaración en lugar de una returndeclaración.

Si una función contiene al menos una yielddeclaración (puede contener otras declaraciones yieldo return), se convierte en una función generadora. Ambos yieldy returndevolverán algún valor de una función.

La diferencia es que mientras que una returndeclaración termina una función por completo, la yielddeclaración detiene la función guardando todos sus estados y luego continúa desde allí en llamadas sucesivas.

Diferencias entre la función de generador y la función normal

Así es como una función generadora se diferencia de una función normal.

  • La función generadora contiene una o más yielddeclaraciones.
  • Cuando se llama, devuelve un objeto (iterador) pero no inicia la ejecución inmediatamente.
  • Los métodos como __iter__()y __next__()se implementan automáticamente. Entonces podemos iterar a través de los elementos usando next().
  • Una vez que la función cede, la función se detiene y el control se transfiere a la persona que llama.
  • Las variables locales y sus estados se recuerdan entre llamadas sucesivas.
  • Finalmente, cuando la función termina, StopIterationse genera automáticamente en más llamadas.

A continuación, se muestra un ejemplo para ilustrar todos los puntos mencionados anteriormente. Tenemos una función generadora nombrada my_gen()con varias yielddeclaraciones.

 # A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n

A continuación se muestra una ejecución interactiva en el intérprete. Ejecútelos en el shell de Python para ver el resultado.

 >>> # It returns an object but does not start execution immediately. >>> a = my_gen() >>> # We can iterate through the items using next(). >>> next(a) This is printed first 1 >>> # Once the function yields, the function is paused and the control is transferred to the caller. >>> # Local variables and theirs states are remembered between successive calls. >>> next(a) This is printed second 2 >>> next(a) This is printed at last 3 >>> # Finally, when the function terminates, StopIteration is raised automatically on further calls. >>> next(a) Traceback (most recent call last):… StopIteration >>> next(a) Traceback (most recent call last):… StopIteration

Una cosa interesante a tener en cuenta en el ejemplo anterior es que el valor de la variable n se recuerda entre cada llamada.

A diferencia de las funciones normales, las variables locales no se destruyen cuando la función cede. Además, el objeto generador se puede iterar solo una vez.

Para reiniciar el proceso, necesitamos crear otro objeto generador usando algo como a = my_gen().

Una última cosa a tener en cuenta es que podemos usar generadores con bucles for directamente.

Esto se debe a que un forbucle toma un iterador y lo recorre con la next()función. Termina automáticamente cuando StopIterationse levanta. Consulte aquí para saber cómo se implementa realmente un bucle for en Python.

 # A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n # Using for loop for item in my_gen(): print(item)

Cuando ejecute el programa, la salida será:

 Esto se imprime primero 1 Esto se imprime en segundo lugar 2 Esto se imprime al final 3

Generadores de Python con un bucle

El ejemplo anterior es de menos utilidad y lo estudiamos solo para tener una idea de lo que estaba sucediendo en segundo plano.

Normalmente, las funciones del generador se implementan con un bucle que tiene una condición de terminación adecuada.

Tomemos un ejemplo de un generador que invierte una cadena.

 def rev_str(my_str): length = len(my_str) for i in range(length - 1, -1, -1): yield my_str(i) # For loop to reverse the string for char in rev_str("hello"): print(char)

Salida

 olleh

En este ejemplo, hemos usado la range()función para obtener el índice en orden inverso usando el ciclo for.

Nota : Esta función de generador no solo funciona con cadenas, sino también con otros tipos de iterables como lista, tupla, etc.

Expresión del generador de Python

Los generadores simples se pueden crear fácilmente sobre la marcha utilizando expresiones de generador. Facilita la construcción de generadores.

Similar a las funciones lambda que crean funciones anónimas, las expresiones generadoras crean funciones generadoras anónimas.

La sintaxis de la expresión del generador es similar a la de una lista de comprensión en Python. Pero los corchetes se reemplazan por paréntesis redondos.

La principal diferencia entre una comprensión de lista y una expresión generadora es que una comprensión de lista produce la lista completa mientras que la expresión generadora produce un elemento a la vez.

They have lazy execution ( producing items only when asked for ). For this reason, a generator expression is much more memory efficient than an equivalent list comprehension.

 # Initialize the list my_list = (1, 3, 6, 10) # square each term using list comprehension list_ = (x**2 for x in my_list) # same thing can be done using a generator expression # generator expressions are surrounded by parenthesis () generator = (x**2 for x in my_list) print(list_) print(generator)

Output

 (1, 9, 36, 100) 

We can see above that the generator expression did not produce the required result immediately. Instead, it returned a generator object, which produces items only on demand.

Here is how we can start getting items from the generator:

 # Initialize the list my_list = (1, 3, 6, 10) a = (x**2 for x in my_list) print(next(a)) print(next(a)) print(next(a)) print(next(a)) next(a)

When we run the above program, we get the following output:

 1 9 36 100 Traceback (most recent call last): File "", line 15, in StopIteration

Generator expressions can be used as function arguments. When used in such a way, the round parentheses can be dropped.

 >>> sum(x**2 for x in my_list) 146 >>> max(x**2 for x in my_list) 100

Use of Python Generators

There are several reasons that make generators a powerful implementation.

1. Easy to Implement

Generators can be implemented in a clear and concise way as compared to their iterator class counterpart. Following is an example to implement a sequence of power of 2 using an iterator class.

 class PowTwo: def __init__(self, max=0): self.n = 0 self.max = max def __iter__(self): return self def __next__(self): if self.n> self.max: raise StopIteration result = 2 ** self.n self.n += 1 return result

The above program was lengthy and confusing. Now, let's do the same using a generator function.

 def PowTwoGen(max=0): n = 0 while n < max: yield 2 ** n n += 1

Since generators keep track of details automatically, the implementation was concise and much cleaner.

2. Memory Efficient

A normal function to return a sequence will create the entire sequence in memory before returning the result. This is an overkill, if the number of items in the sequence is very large.

Generator implementation of such sequences is memory friendly and is preferred since it only produces one item at a time.

3. Represent Infinite Stream

Los generadores son un medio excelente para representar un flujo infinito de datos. Los flujos infinitos no se pueden almacenar en la memoria y, dado que los generadores producen solo un elemento a la vez, pueden representar un flujo infinito de datos.

La siguiente función generadora puede generar todos los números pares (al menos en teoría).

 def all_even(): n = 0 while True: yield n n += 2

4. Generadores de tuberías

Se pueden utilizar varios generadores para canalizar una serie de operaciones. Esto se ilustra mejor con un ejemplo.

Suponga que tenemos un generador que produce los números de la serie de Fibonacci. Y tenemos otro generador para elevar números al cuadrado.

Si queremos averiguar la suma de los cuadrados de los números en la serie de Fibonacci, podemos hacerlo de la siguiente manera canalizando la salida de las funciones generadoras juntas.

 def fibonacci_numbers(nums): x, y = 0, 1 for _ in range(nums): x, y = y, x+y yield x def square(nums): for num in nums: yield num**2 print(sum(square(fibonacci_numbers(10))))

Salida

 4895

Esta canalización es eficiente y fácil de leer (y sí, ¡mucho más genial!).

Articulos interesantes...