Comprender la palabra clave “rendimiento” de Python

C

los 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. Al usar 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 aumento en el rendimiento.

En este artículo explicaremos cómo usar el yield palabra clave en Python y lo que hace exactamente. Pero primero, estudiemos la diferencia entre una colección de listas simple y un generador, y luego veremos cómo yield se puede utilizar 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 en qué se diferencian. Primero crearemos una lista simple y comprobaremos su tipo:

# 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 del squared_list.

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

El script anterior producirá los siguientes resultados:

$ python squared_list.py 
0
1
4
9
16

Ahora creemos un generador y realicemos exactamente la misma tarea:

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

# Check the type
type(squared_gen)

Para crear un generador, comience 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 squared_gen variable. Ahora iteremos sobre el generador usando un bucle for.

for number in squared_gen:
    print(number)

La salida será:

$ python squared_gen.py 
0
1
4
9
16

La salida es la misma que la 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 se mueven 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. los len(squared_list) devolverá 5 mientras 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 iterar nuevamente, debe crear el generador nuevamente.

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 usando el estilo de comprensión de listas. Sin embargo, en escenarios más complejos, podemos crear funciones que devuelvan un generador. los yield palabra clave, a diferencia de la return declaración, se utiliza para convertir una función Python normal 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, veamos primero qué devuelve nuestra función si no usamos el yield palabra clave. Ejecute el siguiente script:

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 una función cube_numbers Se crea que acepta una lista de números, toma sus cubos y devuelve la lista completa al llamante. Cuando se llama a esta función, se devuelve una lista de cubos y se almacena en el cubes variable. Puede ver en la salida que los datos devueltos son de hecho una lista completa:

$ 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.

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, el cube_numbers La función devuelve un generador en lugar de una lista de números al cubo. Es muy simple crear un generador usando el yield palabra clave. Aquí no necesitamos el temporal cube_list variable para almacenar el número al cubo, por lo que incluso nuestro cube_numbers El método es más sencillo. También no return se necesita una declaración, pero en cambio la yield La palabra clave se usa para devolver el número al cubo dentro del bucle for.

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

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

A pesar de que llamamos al cube_numbers función, 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 la función integrada next método. Cuando llamas al next iterador en el generador por primera vez, la función se ejecuta hasta que el yield se encuentra la palabra clave. Una vez yield se encuentra el valor que se le pasa se devuelve a la función que llama y la función generadora se pausa en su estado actual.

Así es como obtiene un valor de su generador:

next(cubes)

La función anterior devolverá “1”. Ahora cuando llamas next de nuevo en el generador, el cube_numbers La función reanudará la ejecución desde donde se detuvo anteriormente en yield. La función continuará ejecutándose hasta que encuentre yield otra vez. los next La función seguirá devolviendo el valor al cubo uno por uno hasta que se repitan todos los valores de la lista.

Una vez que se repiten todos los valores, next función lanza un StopIteration excepción. Es importante mencionar que el cubes El generador no almacena ninguno de estos elementos en la memoria, sino que los valores al cubo se calculan en tiempo de ejecución, se devuelven y se olvidan. La única memoria adicional que se utiliza son los datos de estado del propio generador, que suele ser mucho menor 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 next iterador, en su lugar puede utilizar un bucle “for” para iterar sobre los valores de un generador. Cuando se utiliza un bucle “for”, detrás de escena el next Se llama al 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. articulo.

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 la lista eran muy pequeños. En esta sección veremos algunos ejemplos en los que podemos distinguir entre el rendimiento de listas y generadores.

En el siguiente código, escribiremos una función que devuelve una lista que contiene 1 millón de dummy car objetos. Calcularemos la memoria que ocupa el proceso antes y después de llamar a la función (que crea la lista).

Eche un vistazo al siguiente código:

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: Puede que tengas que pip install psutil para que este código funcione en su 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):

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

Antes de crear la lista, la memoria de proceso se 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 llevó crear la lista fue de 1,58 segundos.

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

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 for car in car_list_gen(1000000) bucle para garantizar que se generen todos los 1000000 coches.

Se obtuvieron los siguientes resultados ejecutando el script anterior:

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

En la salida, 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 fue un poco más rápido también en 1,37 segundos, que es aproximadamente un 14% más rápido que la creación de la lista.

Conclusión

Esperamos que de este artículo tenga una mejor comprensión de la yield palabra clave, incluido cómo se usa, para qué se usa y por qué le gustaría usarla. Los generadores de Python son una excelente manera de mejorar el rendimiento de sus programas y son muy simples de usar, pero comprender cuándo usarlos es un desafío para muchos programadores novatos.

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad