Comprender la palabra clave rendimiento de Python

La palabra clave yield en Python se usa para crear generadores. Un generador es un tipo de colección que produce elementos sobre la marcha y solo se puede iterar una vez. por ti...

La palabra clave yield en Python se usa para crear generadores. Un generador es un tipo de colección que produce elementos sobre la marcha y solo se puede iterar una vez. Mediante el uso de generadores, puede mejorar el rendimiento de su aplicación y consumir menos memoria en comparación con las colecciones normales, por lo que proporciona un buen impulso en el rendimiento.

En este artículo, explicaremos cómo usar la palabra clave yield en Python y qué hace exactamente. Pero primero, estudiemos la diferencia entre una colección de listas simple y un generador, y luego veremos cómo se puede usar yield para crear generadores más complejos.

Diferencias entre una lista y un generador

En el siguiente script, crearemos tanto una lista como un generador e intentaremos ver dónde difieren. Primero crearemos una lista simple y verificaremos su tipo:

1
2
3
4
5
# Creating a list using list comprehension
squared_list = [x**2 for x in range(5)]

# Check the type
type(squared_list)

Al ejecutar este código, debería ver que el tipo que se muestra será "lista".

Ahora iteremos sobre todos los elementos en squared_list.

1
2
3
# Iterate over items and print them
for number in squared_list:
    print(number)

El script anterior producirá los siguientes resultados:

1
2
3
4
5
6
$ python squared_list.py 
0
1
4
9
16

Ahora vamos a crear un generador y realizar exactamente la misma tarea:

1
2
3
4
5
# Creating a generator
squared_gen = (x**2 for x in range(5))

# Check the type
type(squared_gen)

Para crear un generador, comienza exactamente como lo haría con la comprensión de listas, pero en su lugar debe usar paréntesis en lugar de corchetes. El script anterior mostrará "generador" como el tipo de variable squared_gen. Ahora vamos a iterar sobre el generador usando un bucle for.

1
2
for number in squared_gen:
    print(number)

La salida será:

1
2
3
4
5
6
$ python squared_gen.py 
0
1
4
9
16

El resultado es el mismo que el de la lista. Entonces cuál es la diferencia? Una de las principales diferencias radica en la forma en que la lista y los generadores almacenan elementos en la memoria. Las listas almacenan todos los elementos en la memoria a la vez, mientras que los generadores "crean" cada elemento sobre la marcha, lo muestran y luego pasan al siguiente elemento, descartando el elemento anterior de la memoria.

Una forma de verificar esto es verificar la longitud tanto de la lista como del generador que acabamos de crear. len(squared_list) devolverá 5 mientras que len(squared_gen) arrojará un error de que un generador no tiene longitud. Además, puede iterar sobre una lista tantas veces como desee, pero puede iterar sobre un generador solo una vez. Para volver a iterar, debe volver a crear el generador.

Uso de la palabra clave de rendimiento

Ahora que conocemos la diferencia entre colecciones simples y generadores, veamos cómo yield puede ayudarnos a definir un generador.

En los ejemplos anteriores, creamos un generador implícitamente utilizando el estilo de comprensión de lista. Sin embargo, en escenarios más complejos, podemos crear funciones que devuelvan un generador. La palabra clave yield, a diferencia de la sentencia return, se utiliza para convertir una función normal de Python en un generador. Esto se usa como una alternativa a devolver una lista completa a la vez. Esto se explicará nuevamente con la ayuda de algunos ejemplos simples.

Nuevamente, primero veamos qué devuelve nuestra función si no usamos la palabra clave yield. Ejecute el siguiente script:

1
2
3
4
5
6
7
8
9
def cube_numbers(nums):
    cube_list =[]
    for i in nums:
        cube_list.append(i**3)
    return cube_list

cubes = cube_numbers([1, 2, 3, 4, 5])

print(cubes)

En este script se crea una función cube_numbers que acepta una lista de números, toma sus cubos y devuelve la lista completa a la persona que llama. Cuando se llama a esta función, se devuelve una lista de cubos y se almacena en la variable cubes. Puede ver en el resultado que los datos devueltos son, de hecho, una lista completa:

1
2
$ python cubes_list.py 
[1, 8, 27, 64, 125]

Ahora, en lugar de devolver una lista, modifiquemos el script anterior para que devuelva un generador.

1
2
3
4
5
6
7
def cube_numbers(nums):
    for i in nums:
        yield(i**3)

cubes = cube_numbers([1, 2, 3, 4, 5])

print(cubes)

En el script anterior, la función cube_numbers devuelve un generador en lugar de una lista de números al cubo. Es muy simple crear un generador usando la palabra clave yield. Aquí no necesitamos la variable temporal cube_list para almacenar números al cubo, por lo que incluso nuestro método cube_numbers es más simple. Además, no se necesita una declaración return, sino que se usa la palabra clave yield para devolver el número al cubo dentro del bucle for.

Ahora, cuando se llama a la función cube_number, se devuelve un generador, que podemos verificar ejecutando el código:

1
2
$ python cubes_gen.py 
<generator object cube_numbers at 0x1087f1230>

Aunque llamamos a la función cube_numbers, en realidad no se ejecuta en este momento, y todavía no hay elementos almacenados en la memoria.

Para que la función se ejecute y, por lo tanto, el siguiente elemento del generador, usamos el método integrado siguiente. Cuando llama al iterador next en el generador por primera vez, la función se ejecuta hasta que se encuentra la palabra clave yield. Una vez que se encuentra el rendimiento, el valor que se le pasa se devuelve a la función de llamada y la función del generador se detiene en su estado actual.

Así es como obtienes un valor de tu generador:

1
next(cubes)

La función anterior devolverá "1". Ahora, cuando vuelva a llamar a next en el generador, la función cube_numbers reanudará la ejecución desde donde se detuvo anteriormente en yield. La función continuará ejecutándose hasta que encuentre rendimiento nuevamente. La función siguiente seguirá devolviendo el valor al cubo uno por uno hasta que se iteren todos los valores de la lista.

Una vez que se iteran todos los valores, la función siguiente lanza una excepción Detener iteración. Es importante mencionar que el generador de cubos no almacena ninguno de estos elementos en la memoria, sino que los valores en cubos se calculan en tiempo de ejecución, se devuelven y se olvidan. La única memoria adicional utilizada son los datos de estado del propio generador, que suele ser mucho menos que una lista grande. Esto hace que los generadores sean ideales para tareas que requieren mucha memoria.

En lugar de tener que usar siempre el iterador siguiente, puede usar un bucle "for" para iterar sobre los valores de un generador. Cuando se usa un bucle "for", en segundo plano se llama al siguiente iterador hasta que se repiten todos los elementos del generador.

Rendimiento optimizado

Como se mencionó anteriormente, los generadores son muy útiles cuando se trata de tareas que requieren mucha memoria, ya que no necesitan almacenar todos los elementos de la colección en la memoria, sino que generan elementos sobre la marcha y los descartan tan pronto como el iterador pasa al siguiente. artículo.

En los ejemplos anteriores, la diferencia de rendimiento de una lista simple y un generador no era visible debido a que los tamaños de lista eran muy pequeños. En esta sección, veremos algunos ejemplos en los que podemos distinguir entre el rendimiento de las listas y los generadores.

En el siguiente código, escribiremos una función que devuelva una lista que contenga 1 millón de objetos ficticios coche. Calcularemos la memoria ocupada por el proceso antes y después de llamar a la función (que crea la lista).

Echa un vistazo al siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import time
import random
import os
import psutil

car_names = ['Audi', 'Toyota', 'Renault', 'Nissan', 'Honda', 'Suzuki']
colors = ['Black', 'Blue', 'Red', 'White', 'Yellow']

def car_list(cars):
    all_cars = []
    for i in range(cars):
        car = {
            'id': i,
            'name': random.choice(car_names),
            'color': random.choice(colors)
        }
        all_cars.append(car)
    return all_cars

# Get used memory
process = psutil.Process(os.getpid())
print('Memory before list is created: ' + str(process.memory_info().rss/1000000))

# Call the car_list function and time how long it takes
t1 = time.clock()
cars = car_list(1000000)
t2 = time.clock()

# Get used memory
process = psutil.Process(os.getpid())
print('Memory after list is created: ' + str(process.memory_info().rss/1000000))

print('Took {} seconds'.format(t2-t1))

Nota: Es posible que tengas que pip install psutil para que este código funcione en tu máquina.

En la máquina en la que se ejecutó el código, se obtuvieron los siguientes resultados (el suyo puede verse ligeramente diferente):

1
2
3
4
$ python perf_list.py 
Memory before list is created: 8
Memory after list is created: 334
Took 1.584018 seconds

Antes de que se creara la lista, la memoria de proceso era de 8 MB, y después de la creación de la lista con 1 millón de elementos, la memoria ocupada saltó a 334 MB. Además, el tiempo que se tardó en crear la lista fue de 1,58 segundos.

Ahora, repitamos el proceso anterior pero reemplacemos la lista con generador. Ejecute el siguiente script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import time
import random
import os
import psutil

car_names = ['Audi', 'Toyota', 'Renault', 'Nissan', 'Honda', 'Suzuki']
colors = ['Black', 'Blue', 'Red', 'White', 'Yellow']

def car_list_gen(cars):
    for i in range(cars):
        car = {
            'id':i,
            'name':random.choice(car_names),
            'color':random.choice(colors)
        }
        yield car

# Get used memory
process = psutil.Process(os.getpid())
print('Memory before list is created: ' + str(process.memory_info().rss/1000000))

# Call the car_list_gen function and time how long it takes
t1 = time.clock()
for car in car_list_gen(1000000):
    pass
t2 = time.clock()

# Get used memory
process = psutil.Process(os.getpid())
print('Memory after list is created: ' + str(process.memory_info().rss/1000000))

print('Took {} seconds'.format(t2-t1))

Aquí tenemos que usar el bucle for car in car_list_gen(1000000) para garantizar que todos los 1000000 autos se generen realmente.

Los siguientes resultados se obtuvieron al ejecutar el script anterior:

1
2
3
4
$ python perf_gen.py 
Memory before list is created: 8
Memory after list is created: 40
Took 1.365244 seconds

En el resultado, puede ver que al usar generadores, la diferencia de memoria es mucho menor que antes (de 8 MB a 40 MB) ya que los generadores no almacenan los elementos en la memoria. Además, el tiempo necesario para llamar a la función del generador también fue un poco más rápido, 1,37 segundos, que es aproximadamente un 14 % más rápido que la creación de la lista.

Conclusión

Esperamos que gracias a este artículo comprendas mejor la palabra clave yield, incluido cómo se usa, para qué se usa y por qué querrías usarla. Generadores de Python son una excelente manera de mejorar el rendimiento de sus programas y son muy simples de usar, pero entender cuándo usarlos es el desafío para muchos programadores novatos.

Licensed under CC BY-NC-SA 4.0