Uso de generadores para ahorros sustanciales de memoria en Python

Cuando la administración de la memoria y el mantenimiento del estado entre el valor generado se convirtieron en un trabajo difícil para los programadores, Python implementó una solución amigable llamada Generadores .

Generators

Generadores

Con los generadores, las funciones evolucionan para acceder y calcular datos en partes . Por lo tanto, las funciones pueden devolver el resultado a su llamador a pedido y pueden mantener su estado. Los generadores mantienen el estado de la función al detener el código después de producir el valor para la persona que llama y, a pedido, continúa la ejecución desde donde se quedó.
Dado que el generador accede y calcula el valor a pedido, una gran parte de los datos no necesita guardarse en la memoria por completo y da como resultado un ahorro sustancial de memoria.

Sintaxis del generador

yield statement

declaración de rendimiento

Podemos decir que una función es un generador cuando tiene una declaración de rendimiento dentro del código. Al igual que en una declaración de devolución, la declaración de rendimiento también envía un valor a la persona que llama, pero no sale de la ejecución de la función. En su lugar, detiene la ejecución hasta que se recibe la siguiente solicitud. A pedido, el generador continúa ejecutándose desde donde se quedó.

def primeFunction():
    prime = None
    num = 1
    while True:
        num = num + 1
  
        for i in range(2, num): 
            if(num % i) == 0:
                prime = False
                break
            else:
                prime = True
  
        if prime:
              
            # yields the value to the caller 
            # and halts the execution
            yield num
  
def main():
      
    # returns the generator object.
    prime = primeFunction()
      
    # generator executes upon request
    for i in prime:
        print(i)
        if i > 50:
            break
  
if __name__ == "__main__":
    main()

Producción

3
5
7
11
13
17
19
23
29
31
37
41
43
47
53

Comunicación con generador

next, stopIteration and send

siguiente, detener la iteración y enviar

¿Cómo se comunican entre sí la persona que llama y el generador? Aquí discutiremos 3 funciones integradas en python. Están:

  • Siguiente
  • detener iteración
  • enviar

Siguiente

La siguiente función puede solicitar un generador para su próximo valor. A pedido, el código del generador se ejecuta y la declaración de rendimiento proporciona el valor a la persona que llama. En este punto, el generador detiene la ejecución y espera la siguiente solicitud. Profundicemos más considerando una función de Fibonacci.

def fibonacci():
    values = []
    while True:
  
        if len(values) < 2:
            values.append(1)
        else :
              
            # sum up the values and 
            # append the result
            values.append(sum(values))
              
            # pop the first value in 
            # the list
            values.pop(0)
  
        # yield the latest value to 
        # the caller
        yield values[-1]
        continue
  
def main():
    fib = fibonacci()
    print(next(fib))  # 1
    print(next(fib))  # 1
    print(next(fib))  # 2
    print(next(fib))  # 3
    print(next(fib))  # 5
  
if __name__ == "__main__":
    main()

Producción

1
1
2
3
5
  • Crear el objeto generador llamando a la función fibonacci y guardando su valor devuelto en fib. En este caso, el código no se ha ejecutado, el intérprete de python reconoce el generador y devuelve el objeto generador. Dado que la función tiene una declaración de rendimiento, se devuelve el objeto generador en lugar de un valor.

    fib = fibonacci()
    fib

    Producción

    generator object fibonacci at 0x00000157F8AA87C8
  • Usando la siguiente función, la persona que llama solicita un valor al generador y comienza la ejecución.

    next(gen)
    Producción

    1
  • Dado que la lista de valores está vacía, se ejecuta el código dentro de la ‘sentencia if’ y se agrega ‘1’ a la lista de valores. A continuación, el valor se entrega a la persona que llama mediante la declaración de rendimiento y la ejecución se detiene. El punto a tener en cuenta aquí es que la ejecución se detiene antes de ejecutar la declaración de continuación.

    # values = []
    if len(values) < 2:
      
        # values = [1]
        values.append(1)
      
    # 1
    yield values[-1]
    continue
  • Tras la segunda solicitud, el código continúa la ejecución desde donde lo dejó. Aquí se ejecuta desde la sentencia `continuar` y pasa el control al bucle while.
    Ahora la lista de valores contiene un valor de la primera solicitud. Dado que la longitud de los ‘valores’ es 1 y menor que 2, se ejecuta el código dentro de la ‘instrucción if’.

    # values = [1]
    if len(values) < 2:
      
        # values = [1, 1]
        values.append(1)
      
    # 1 (latest value is provided 
    # to the caller)
    yield values[-1]
    continue
  • Nuevamente, el valor se solicita usando next(fib) y la ejecución comienza desde la instrucción `continuar`. Ahora, la longitud de los valores no es inferior a 2. Por lo tanto, ingresa la declaración else y suma los valores en la lista y agrega el resultado. La instrucción pop elimina el primer elemento de la lista y produce el resultado más reciente.

    # values = [1, 1]
    else:
          
        # values = [1, 1, 2]
        values.append(sum(values))
          
        # values = [1, 2]
        values.pop(0)
          
    # 2
    yield values[-1]
    continue
  • Su solicitud de más valores repetirá el patrón y producirá el último valor

Detener iteración

StopIteration es una excepción integrada que se usa para salir de un generador. Cuando se completa la iteración del generador, envía una señal a la persona que llama generando la excepción StopIteration y sale.

El siguiente código explica el escenario.

def stopIteration():
    num = 5
    for i in range(1, num):
        yield i
  
def main():
    f = stopIteration()
      
    # 1 is generated
    print(next(f))
      
    # 2 is generated
    print(next(f))
      
    # 3 is generated
    print(next(f))
      
    # 4 is generated
    print(next(f))
      
    # 5th element - raises
    # StopIteration Exception
    next(f)
  
if __name__ == "__main__":
    main()
      
     

Producción

1
2
3
4
Rastreo (última llamada más reciente):
Archivo “C:\Users\Sonu George\Documents\GeeksforGeeks\Python Pro\Generators\stopIteration.py”, línea 19, en
main()
Archivo “C:\Users\ Sonu George\Documents\GeeksforGeeks\Python Pro\Generators\stopIteration.py”, línea 15, en main
next(f) # 5th element – ​​genera StopIteration Exception
StopIteration

El siguiente código explica otro escenario, donde un programador puede generar StopIteration y salir del generador.
raise StopIteration

def stopIteration():
    num = 5
    for i in range(1, num):
        if i == 3:
            raise StopIteration
        yield i
          
  
def main():
    f = stopIteration()
      
    # 1 is generated
    print(next(f))
      
    # 2 is generated
    print(next(f))
      
    # StopIteration raises and 
    # code exits
    print(next(f))
    print(next(f))    
      
  
if __name__ == "__main__":
    main()

Producción

1
2
Rastreo (última llamada más reciente):
Archivo “C:\Users\Sonu George\Documents\GeeksforGeeks\Python Pro\Generators\stopIteration.py”, línea 5, en stopIteration
aumentar StopIteration
StopIteration

La excepción anterior fue la causa directa de la siguiente excepción:

Rastreo (última llamada más reciente):
Archivo «C:\Users\Sonu George\Documents\GeeksforGeeks\Python Pro\Generators\stopIteration.py», línea 19, en
main()
Archivo «C:\Users\Sonu George\Documents \GeeksforGeeks\Python Pro\Generators\stopIteration.py”, línea 13, en la
impresión principal (siguiente (f)) # StopIteration aumenta y el código sale
RuntimeError: generador generado StopIteration

enviar

Hasta ahora, hemos visto cómo el generador entrega valores al código de invocación donde la comunicación es unidireccional. A partir de ahora, el generador no ha recibido ningún dato de la persona que llama.
En esta sección, discutiremos el método `send` que permite que la persona que llama se comunique con el generador.

def factorial():
    num = 1
    while True:
        factorial = 1
  
        for i in range(1, num + 1):
              
            # determines the factorial
            factorial = factorial * i
              
        # produce the factorial to the caller
        response = yield factorial
  
        # if the response has value
        if response:
              
            # assigns the response to 
            # num variable
            num = int(response)
        else:
              
            # num variable is incremented
            # by 1
            num = num + 1
  
def main():
    fact = factorial()
    print(next(fact))
    print(next(fact))
    print(next(fact))
    print(fact.send(5))   # send
    print(next(fact))
  
if __name__ == "__main__":
    main()

Producción

1
2
6
120
720

El generador produce los primeros tres valores (1, 2 y 6) en función de la solicitud de la persona que llama (utilizando el siguiente método) y el cuarto valor (120) se produce en función de los datos (5) proporcionados por la persona que llama (utilizando send método).
Consideremos el tercer dato (6) que arroja el generador. Factorial de 3 = 3*2*1, que arroja el generador y se detiene la ejecución.

factorial = factorial * i 

En este punto, la persona que llama utiliza el método `send` y proporciona los datos ‘5`. Por lo tanto, el generador se ejecuta desde donde se dejó, es decir, guarda los datos enviados por la persona que llama a la variable `respuesta` ( response = yield factorial). Dado que la `respuesta` contiene un valor, el código ingresa la condición `si` y asigna la respuesta a la variable `num`.

if response:
    num = int(response)

Ahora el flujo pasa al ciclo `while` y determina el factorial y se entrega a la persona que llama. Nuevamente, el generador detiene la ejecución hasta la próxima solicitud.

Si observamos el resultado, podemos ver que la orden se interrumpió después de que la persona que llamó usó el método «enviar». Más precisamente, dentro de los primeros 3 pide la salida de la siguiente manera:
Factorial de 1 = 1
Factorial de 2 = 2
Factorial de 3 = 6

Pero cuando el usuario envía el valor 5, la salida se convierte en 120 y `num` mantiene el valor 5. En la siguiente solicitud (usando `siguiente`) esperamos que num se incremente en función de la última solicitud `siguiente` (es decir, 3+1 = 4) en lugar del método `send`. Pero en este caso, `num` se incrementa a 6 (basado en el último valor usando `send`) y produce la salida 720.

El siguiente código muestra un enfoque diferente en el manejo de valores enviados por la persona que llama.

def factorial():
    num = 0
    value = None
    response = None
    while True:
        factorial = 1       
        if response:
            value = int(response)
        else:
            num = num + 1
            value = num
          
        for i in range(1, value + 1):
            factorial = factorial * i
        response = yield factorial
         
  
def main():
    fact = factorial()
    print(next(fact))
    print(next(fact))
    print(next(fact))
    print(fact.send(5))   # send
    print(next(fact))
  
if __name__ == "__main__":
    main()

Producción

1
2
6
120
24

Biblioteca estándar: generadores

Standard Library

Biblioteca estándar

  • rango
  • dict.items
  • Código Postal
  • mapa
  • Objetos de archivo

rango

La función de rango devuelve un objeto de rango iterable y su iterador es un generador. Devuelve el valor secuencial que comienza desde el límite inferior y continúa hasta que se alcanza el límite superior.

def range_func():
    r = range(0, 4)
    return r
  
def main():
    r = range_func()
    iterator = iter(r)
    print(next(iterator))
    print(next(iterator))
  
if __name__ == "__main__":
    main()

Producción

0
1

dict.items

La clase de diccionario en python proporciona tres métodos iterables para iterar el diccionario. Son claves, valores y elementos y sus iteradores son generadores.

def dict_func():
    dictionary = {'UserName': 'abc', 'Password':'a@123'}
    return dictionary
  
def main():
    d = dict_func()
    iterator = iter(d.items())
    print(next(iterator))
    print(next(iterator))
  
if __name__ == "__main__":
    main()

Producción

('UserName', 'abc')
('Password', 'a@123')

Código Postal

zip es una función de python incorporada que toma múltiples objetos iterables y los itera todos a la vez. Producen el primer elemento de cada iterable, luego el segundo y así sucesivamente.

def zip_func():
    z = zip(['a', 'b', 'c', 'd'], [1, 2, 3, 4])
    return z
  
def main():
    z = zip_func()
    print(next(z))
    print(next(z))
    print(next(z))
  
if __name__ == "__main__":
    main()

Producción

('a', 1)
('b', 2)
('c', 3)

mapa

La función map toma la función y los iterables como parámetros y calcula el resultado de la función para cada elemento del iterable.

def map_func():
    m = map(lambda x, y: max([x, y]), [8, 2, 9], [5, 3, 7])
    return m
  
def main():
    m = map_func()
    print(next(m))  # 8 (maximum value among 8 and 5)
    print(next(m))  # 3 (maximum value among 2 and 3)
    print(next(m))  # 9 (maximum value among 9 and 7)
  
if __name__ == "__main__":
    main()

Producción

8
3
9

Objeto de archivo

Aunque el objeto de archivo tiene un método readline para leer el archivo línea por línea, es compatible con el patrón generador. Una diferencia es que aquí el método readline detecta la excepción StopIteration y devuelve una string vacía una vez que se alcanza el final del archivo, lo cual es diferente al usar el siguiente método.

Al usar el siguiente método, el objeto de archivo produce la línea completa, incluido el carácter de nueva línea (\n)

def file_func():
    f = open('sample.txt')
    return f
  
def main():
    f = file_func()
    print(next(f))
    print(next(f))
      
if __name__ == "__main__":
    main()

Entrada: muestra.txt

Rule 1
Rule 2
Rule 3
Rule 4

Producción

Rule 1

Rule 2

Caso de uso de generadores

Generator Use Cases

Casos de uso del generador

El concepto fundamental de Generador es determinar el valor bajo demanda. A continuación, analizaremos dos casos de uso que se derivan del concepto anterior.

  • Acceder a los datos en partes
  • Cómputo de datos en piezas

Acceder a los datos en partes

¿Por qué necesitamos acceder a los datos en partes? La pregunta es válida cuando el programador tiene que manejar una gran cantidad de datos, por ejemplo, leer un archivo, etc. En este caso, hacer una copia de los datos y procesarlos no es una solución factible. Mediante el uso de generadores, los programadores pueden acceder a los datos de uno en uno. Al considerar la operación de archivos, el usuario puede acceder a los datos línea por línea y, en el caso de un diccionario, dos tuplas a la vez.
Por lo tanto, Generator es una herramienta esencial para manejar una gran cantidad de datos que evita el almacenamiento innecesario de los datos y da como resultado un ahorro sustancial de memoria.

Cómputo de datos en piezas

Otra razón para escribir un generador es su capacidad para calcular datos a pedido. De la función de Fibonacci anterior, se puede entender que el generador produce el valor bajo demanda. Este proceso evita la computación y el almacenamiento innecesarios de los valores y, por lo tanto, puede aumentar el rendimiento y también da como resultado un ahorro sustancial de memoria.
Otro punto a tener en cuenta es la capacidad del generador para calcular un número infinito de datos.

Delegación Generador

yield from

rendimiento de

El generador puede invocar a otro generador como lo hace una función. Usando la declaración ‘rendimiento de’, un generador puede lograr esto, y el proceso se llama Delegación del Generador.
Dado que el generador está delegando a otro generador, los valores enviados al generador de envoltura estarán disponibles para el generador delegado actual .

def gensub1():
    yield 'A'
    yield 'B'
  
def gensub2():
    yield '100'
    yield '200'
  
def main_gen():
    yield from gensub1()
    yield from gensub2()
      
def main():
    delg = main_gen()
    print(next(delg))
    print(next(delg))
    print(next(delg))
    print(next(delg))
  
if __name__ == "__main__":
    main()

Producción

A
B
100
200

Resumen

Un generador es una herramienta esencial para los programadores que manejan grandes cantidades de datos. Su capacidad para calcular y acceder a datos a pedido da como resultado un aumento en el rendimiento y ahorro de memoria. Y también, considere usar generadores cuando sea necesario representar una secuencia infinita.

Publicación traducida automáticamente

Artículo escrito por SonuGeorge y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

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