Comprender la palabra clave “rendimiento” de Python

    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.

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *