Buscar..


Introducción

Los generadores son iteradores perezosos creados por las funciones del generador (que utilizan el yield ) o las expresiones del generador (que usan (an_expression for x in an_iterator) ).

Sintaxis

  • rendimiento <expr>
  • rendimiento de <expr>
  • <var> = rendimiento <expr>
  • siguiente ( <iter> )

Iteración

Un objeto generador soporta el protocolo iterador . Es decir, proporciona un método next() ( __next__() en Python 3.x), que se utiliza para avanzar en su ejecución, y su método __iter__ devuelve. Esto significa que se puede usar un generador en cualquier construcción de lenguaje que admita objetos iterables genéricos.

# naive partial implementation of the Python 2.x xrange()
def xrange(n):
    i = 0
    while i < n:
        yield i
        i += 1

# looping
for i in xrange(10):
    print(i)  # prints the values 0, 1, ..., 9

# unpacking
a, b, c = xrange(3)  # 0, 1, 2

# building a list
l = list(xrange(10))  # [0, 1, ..., 9]

La siguiente función ()

El next() incorporado es un envoltorio conveniente que se puede usar para recibir un valor de cualquier iterador (incluido un iterador generador) y para proporcionar un valor predeterminado en caso de que se agote el iterador.

def nums():
    yield 1
    yield 2
    yield 3
generator = nums()

next(generator, None)  # 1
next(generator, None)  # 2
next(generator, None)  # 3
next(generator, None)  # None
next(generator, None)  # None
# ...

La sintaxis es la next(iterator[, default]) . Si el iterador finaliza y se pasa un valor predeterminado, se devuelve. Si no se proporcionó ningún valor predeterminado, se StopIteration .

Enviando objetos a un generador.

Además de recibir valores de un generador, es posible enviar un objeto a un generador utilizando el método send() .

def accumulator():
    total = 0
    value = None
    while True:
        # receive sent value
        value = yield total
        if value is None: break
        # aggregate values
        total += value

generator = accumulator()

# advance until the first "yield"
next(generator)      # 0

# from this point on, the generator aggregates values
generator.send(1)    # 1
generator.send(10)   # 11
generator.send(100)  # 111
# ...

# Calling next(generator) is equivalent to calling generator.send(None)
next(generator)      # StopIteration

Lo que pasa aquí es lo siguiente:

  • Cuando llama por primera vez al next(generator) , el programa avanza a la primera declaración de yield y devuelve el valor del total en ese punto, que es 0. La ejecución del generador se suspende en este punto.
  • Cuando llama a generator.send(x) , el intérprete toma el argumento x y lo convierte en el valor de retorno de la última declaración de yield , que se asigna al value . El generador continúa como de costumbre, hasta que da el siguiente valor.
  • Cuando finalmente llama a next(generator) , el programa trata esto como si estuviera enviando None al generador. No hay nada especial en None , sin embargo, este ejemplo utiliza None como un valor especial para pedirle al generador que se detenga.

Expresiones generadoras

Es posible crear iteradores de generador utilizando una sintaxis similar a la comprensión.

generator = (i * 2 for i in range(3))

next(generator)  # 0
next(generator)  # 2
next(generator)  # 4
next(generator)  # raises StopIteration

Si a una función no necesariamente se le debe pasar una lista, puede guardar caracteres (y mejorar la legibilidad) colocando una expresión de generador dentro de una llamada de función. El paréntesis de la llamada a la función hace implícitamente que su expresión sea una expresión generadora.

sum(i ** 2 for i in range(4))  # 0^2 + 1^2 + 2^2 + 3^2 = 0 + 1 + 4 + 9 = 14

Además, guardará en la memoria porque en lugar de cargar la lista completa sobre la que está iterando ( [0, 1, 2, 3] en el ejemplo anterior), el generador permite que Python use los valores según sea necesario.

Introducción

Las expresiones de los generadores son similares a las listas, diccionarios y conjuntos de comprensión, pero están entre paréntesis. Los paréntesis no tienen que estar presentes cuando se usan como el único argumento para una llamada de función.

expression = (x**2 for x in range(10))

Este ejemplo genera los 10 primeros cuadrados perfectos, incluido 0 (en el que x = 0).

Las funciones del generador son similares a las funciones regulares, excepto que tienen una o más declaraciones de yield en su cuerpo. Dichas funciones no pueden return ningún valor (sin embargo, se permiten return vacías si desea detener el generador antes).

def function():
    for x in range(10):
        yield x**2

Esta función del generador es equivalente a la expresión del generador anterior, produce el mismo.

Nota : todas las expresiones generadoras tienen sus propias funciones equivalentes , pero no al revés.


Se puede usar una expresión generadora sin paréntesis si ambos paréntesis se repetirían de lo contrario:

sum(i for i in range(10) if i % 2 == 0)   #Output: 20
any(x = 0 for x in foo)                   #Output: True or False depending on foo
type(a > b for a in foo if a % 2 == 1)    #Output: <class 'generator'>

En lugar de:

sum((i for i in range(10) if i % 2 == 0))
any((x = 0 for x in foo))
type((a > b for a in foo if a % 2 == 1))

Pero no:

fooFunction(i for i in range(10) if i % 2 == 0,foo,bar)
return x = 0 for x in foo
barFunction(baz, a > b for a in foo if a % 2 == 1)

Al llamar a una función de generador se genera un objeto generador , que luego se puede iterar. A diferencia de otros tipos de iteradores, los objetos generadores solo se pueden atravesar una vez.

g1 = function()
print(g1)  # Out: <generator object function at 0x1012e1888>

Observe que el cuerpo de un generador no se ejecuta inmediatamente: cuando llama a function() en el ejemplo anterior, devuelve inmediatamente un objeto generador, sin ejecutar siquiera la primera declaración de impresión. Esto permite que los generadores consuman menos memoria que las funciones que devuelven una lista, y permite crear generadores que producen secuencias infinitamente largas.

Por esta razón, los generadores a menudo se utilizan en la ciencia de datos y en otros contextos que involucran grandes cantidades de datos. Otra ventaja es que otro código puede usar inmediatamente los valores generados por un generador, sin esperar a que se produzca la secuencia completa.

Sin embargo, si necesita usar los valores producidos por un generador más de una vez, y si generarlos cuesta más que almacenarlos, puede ser mejor almacenar los valores generados como una list que volver a generar la secuencia. Consulte 'Restablecer un generador' a continuación para obtener más detalles.

Normalmente, un objeto generador se usa en un bucle, o en cualquier función que requiera un iterable:

for x in g1:
    print("Received", x)

# Output:
# Received 0
# Received 1
# Received 4
# Received 9
# Received 16
# Received 25
# Received 36
# Received 49
# Received 64
# Received 81

arr1 = list(g1)
# arr1 = [], because the loop above already consumed all the values.
g2 = function()
arr2 = list(g2)  # arr2 = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Como los objetos generadores son iteradores, uno puede recorrerlos manualmente usando la función next() . Al hacerlo, se devolverán los valores cedidos uno por uno en cada invocación posterior.

Bajo el capó, cada vez que llama a next() en un generador, Python ejecuta declaraciones en el cuerpo de la función del generador hasta que llega a la siguiente declaración de yield . En este punto, devuelve el argumento del comando de yield y recuerda el punto en el que ocurrió. Llamar a next() una vez más reanudará la ejecución desde ese punto y continuará hasta la próxima declaración de yield .

Si Python llega al final de la función del generador sin encontrar más yield , se StopIteration una excepción StopIteration (esto es normal, todos los iteradores se comportan de la misma manera).

g3 = function()
a = next(g3)  # a becomes 0
b = next(g3)  # b becomes 1
c = next(g3)  # c becomes 2
...
j = next(g3)  # Raises StopIteration, j remains undefined

Tenga en cuenta que en el generador Python 2, los objetos tenían métodos .next() que se podían usar para iterar a través de los valores producidos manualmente. En Python 3, este método fue reemplazado por el estándar .__next__() para todos los iteradores.

Restablecer un generador

Recuerde que solo puede recorrer los objetos generados por un generador una vez . Si ya ha iterado a través de los objetos en una secuencia de comandos, cualquier otro intento de hacerlo dará como resultado None .

Si necesita usar los objetos generados por un generador más de una vez, puede definir la función del generador de nuevo y usarla por segunda vez, o bien, puede almacenar la salida de la función del generador en una lista en el primer uso. Volver a definir la función del generador será una buena opción si está manejando grandes volúmenes de datos, y almacenar una lista de todos los elementos de datos ocuparía mucho espacio en el disco. A la inversa, si es costoso generar los artículos inicialmente, es posible que prefiera almacenar los artículos generados en una lista para poder reutilizarlos.

Usando un generador para encontrar los números de Fibonacci

Un caso de uso práctico de un generador es recorrer los valores de una serie infinita. Aquí hay un ejemplo de cómo encontrar los primeros diez términos de la secuencia de Fibonacci .

def fib(a=0, b=1):
    """Generator that yields Fibonacci numbers. `a` and `b` are the seed values"""
    while True:
        yield a
        a, b = b, a + b

f = fib()
print(', '.join(str(next(f)) for _ in range(10)))

0, 1, 1, 2, 3, 5, 8, 13, 21, 34

Secuencias infinitas

Se pueden usar generadores para representar secuencias infinitas:

def integers_starting_from(n):
    while True:
        yield n
        n += 1

natural_numbers = integers_starting_from(1)

La secuencia infinita de números como la anterior también se puede generar con la ayuda de itertools.count . El código anterior se puede escribir como abajo

natural_numbers = itertools.count(1)

Puede usar la comprensión del generador en generadores infinitos para producir nuevos generadores:

multiples_of_two = (x * 2 for x in natural_numbers)
multiples_of_three = (x for x in natural_numbers if x % 3 == 0)

Tenga en cuenta que un generador infinito no tiene un fin, por lo que pasarlo a cualquier función que intente consumir el generador por completo tendrá graves consecuencias :

list(multiples_of_two)  # will never terminate, or raise an OS-specific error

En su lugar, use la lista de listas / conjuntos con range (o xrange para python <3.0):

first_five_multiples_of_three = [next(multiples_of_three) for _ in range(5)] 
# [3, 6, 9, 12, 15]

o use itertools.islice() para cortar el iterador en un subconjunto:

from itertools import islice
multiples_of_four = (x * 4 for x in integers_starting_from(1))
first_five_multiples_of_four = list(islice(multiples_of_four, 5))
# [4, 8, 12, 16, 20]

Tenga en cuenta que el generador original también se actualiza, al igual que todos los demás generadores que vienen de la misma "raíz":

next(natural_numbers)    # yields 16
next(multiples_of_two)   # yields 34
next(multiples_of_four)  # yields 24

Una secuencia infinita también se puede iterar con un bucle for . Asegúrese de incluir una instrucción de break condicional para que el bucle finalice eventualmente:

for idx, number in enumerate(multiplies_of_two):
    print(number)
    if idx == 9:
        break  # stop after taking the first 10 multiplies of two

Ejemplo clásico - números de Fibonacci

import itertools

def fibonacci():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

first_ten_fibs = list(itertools.islice(fibonacci(), 10))
# [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

def nth_fib(n):
    return next(itertools.islice(fibonacci(), n - 1, n))

ninety_nineth_fib = nth_fib(99)  # 354224848179261915075

Rindiendo todos los valores de otro iterable.

Python 3.x 3.3

Utilice el yield from si desea obtener todos los valores de otro iterable:

def foob(x):
    yield from range(x * 2)
    yield from range(2)

list(foob(5))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1]

Esto funciona también con generadores.

def fibto(n):
    a, b = 1, 1
    while True:
        if a >= n: break
        yield a
        a, b = b, a + b

def usefib():
    yield from fibto(10)
    yield from fibto(20)

list(usefib())  # [1, 1, 2, 3, 5, 8, 1, 1, 2, 3, 5, 8, 13]

Coroutines

Los generadores pueden ser utilizados para implementar coroutines:

# create and advance generator to the first yield
def coroutine(func):
    def start(*args,**kwargs):
        cr = func(*args,**kwargs)
        next(cr)
        return cr
    return start

# example coroutine
@coroutine
def adder(sum = 0):
    while True:
        x = yield sum
        sum += x

# example use
s = adder()
s.send(1) # 1
s.send(2) # 3

Coroutines se usa comúnmente para implementar máquinas de estado, ya que son principalmente útiles para crear procedimientos de un solo método que requieren un estado para funcionar correctamente. Operan en un estado existente y devuelven el valor obtenido al finalizar la operación.

Rendimiento con recursión: listado recursivo de todos los archivos en un directorio

Primero, importa las bibliotecas que trabajan con archivos:

from os import listdir
from os.path import isfile, join, exists

Una función auxiliar para leer solo archivos de un directorio:

def get_files(path):
    for file in listdir(path):
        full_path = join(path, file)
        if isfile(full_path):
            if exists(full_path):
                yield full_path

Otra función auxiliar para obtener solo los subdirectorios:

def get_directories(path):
    for directory in listdir(path):
        full_path = join(path, directory)
        if not isfile(full_path):
            if exists(full_path):
                yield full_path

Ahora use estas funciones para obtener recursivamente todos los archivos dentro de un directorio y todos sus subdirectorios (usando generadores):

def get_files_recursive(directory):
    for file in get_files(directory):
        yield file
    for subdirectory in get_directories(directory):
        for file in get_files_recursive(subdirectory): # here the recursive call
            yield file

Esta función se puede simplificar utilizando el yield from :

def get_files_recursive(directory):
    yield from get_files(directory)
    for subdirectory in get_directories(directory):
        yield from get_files_recursive(subdirectory)

Iterando sobre generadores en paralelo.

Para iterar sobre varios generadores en paralelo, use el zip incorporado:

for x, y in zip(a,b):
    print(x,y)

Resultados en:

1 x
2 y
3 z

En Python 2 deberías usar itertools.izip en itertools.izip lugar. Aquí también podemos ver que todas las funciones zip producen tuplas.

Tenga en cuenta que zip dejará de iterar tan pronto como uno de los iterables se quede sin elementos. Si desea iterar durante el tiempo más largo posible, use itertools.zip_longest() .

Código de construcción de lista de refactorización

Supongamos que tiene un código complejo que crea y devuelve una lista al comenzar con una lista en blanco y agregarla repetidamente:

def create():
    result = []
    # logic here...
    result.append(value) # possibly in several places
    # more logic...
    return result # possibly in several places

values = create()

Cuando no es práctico reemplazar la lógica interna con una lista de comprensión, puede convertir toda la función en un generador en el lugar y luego recopilar los resultados:

def create_gen():
    # logic...
    yield value
    # more logic
    return # not needed if at the end of the function, of course

values = list(create_gen())

Si la lógica es recursiva, use el yield from para incluir todos los valores de la llamada recursiva en un resultado "aplanado":

def preorder_traversal(node):
    yield node.value
    for child in node.children:
        yield from preorder_traversal(child)

buscando

La next función es útil incluso sin iterar. Pasar una expresión del generador a la next es una forma rápida de buscar la primera aparición de un elemento que coincida con algún predicado. Código de procedimiento como

def find_and_transform(sequence, predicate, func):
    for element in sequence:
        if predicate(element):
            return func(element)
    raise ValueError

item = find_and_transform(my_sequence, my_predicate, my_func)

puede ser reemplazado con:

item = next(my_func(x) for x in my_sequence if my_predicate(x))
# StopIteration will be raised if there are no matches; this exception can
# be caught and transformed, if desired.

Para este propósito, puede ser conveniente crear un alias, como first = next , o una función de envoltura para convertir la excepción:

def first(generator):
    try:
        return next(generator)
    except StopIteration:
        raise ValueError


Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow