Pregunta

Estoy empezando a aprender Python y me he encontrado con funciones generadoras, aquellas que tienen una declaración de rendimiento.Quiero saber qué tipos de problemas estas funciones son realmente buenas para resolver.

¿Fue útil?

Solución

Los generadores te dan una evaluación perezosa.Los usa iterando sobre ellos, ya sea explícitamente con 'for' o implícitamente pasándolo a cualquier función o construcción que itere.Puede pensar que los generadores devuelven varios elementos, como si devolvieran una lista, pero en lugar de devolverlos todos a la vez, los devuelven uno por uno, y la función del generador se pausa hasta que se solicita el siguiente elemento.

Los generadores son buenos para calcular grandes conjuntos de resultados (en particular, cálculos que involucran bucles en sí) cuando no sabes si vas a necesitar todos los resultados o cuando no deseas asignar memoria para todos los resultados al mismo tiempo. .O para situaciones en las que el generador utiliza otro generador, o consume algún otro recurso, y es más conveniente si eso sucede lo más tarde posible.

Otro uso de los generadores (que en realidad es el mismo) es reemplazar las devoluciones de llamada con iteraciones.En algunas situaciones, desea que una función haga mucho trabajo y ocasionalmente informe a la persona que llama.Tradicionalmente usarías una función de devolución de llamada para esto.Pasas esta devolución de llamada a la función de trabajo y ésta llamará periódicamente a esta devolución de llamada.El enfoque del generador es que la función de trabajo (ahora un generador) no sabe nada sobre la devolución de llamada y simplemente cede cuando quiere informar algo.La persona que llama, en lugar de escribir una devolución de llamada separada y pasarla a la función de trabajo, realiza todo el trabajo de informes en un pequeño bucle 'for' alrededor del generador.

Por ejemplo, digamos que escribió un programa de 'búsqueda de sistema de archivos'.Podrías realizar la búsqueda en su totalidad, recopilar los resultados y luego mostrarlos uno a la vez.Todos los resultados tendrían que recopilarse antes de mostrar el primero y todos los resultados estarían en la memoria al mismo tiempo.O podría mostrar los resultados mientras los encuentra, lo que ahorraría más memoria y sería mucho más amigable para el usuario.Esto último podría hacerse pasando la función de impresión de resultados a la función de búsqueda del sistema de archivos, o podría hacerse simplemente convirtiendo la función de búsqueda en un generador e iterando sobre el resultado.

Si desea ver un ejemplo de los dos últimos enfoques, consulte os.path.walk() (la antigua función de seguimiento del sistema de archivos con devolución de llamada) y os.walk() (el nuevo generador de seguimiento del sistema de archivos). realmente querías recopilar todos los resultados en una lista, el enfoque del generador es trivial para convertirlo al enfoque de lista grande:

big_list = list(the_generator)

Otros consejos

Una de las razones para utilizar el generador es aclarar la solución para algunos tipos de soluciones.

La otra es tratar los resultados uno por uno, evitando crear listas enormes de resultados que de todos modos procesaría por separado.

Si tienes una función de Fibonacci hasta n como esta:

# function version
def fibon(n):
    a = b = 1
    result = []
    for i in xrange(n):
        result.append(a)
        a, b = b, a + b
    return result

Puedes escribir más fácilmente la función como esta:

# generator version
def fibon(n):
    a = b = 1
    for i in xrange(n):
        yield a
        a, b = b, a + b

La función es más clara.Y si usas la función así:

for x in fibon(1000000):
    print x,

En este ejemplo, si se utiliza la versión del generador, no se creará la lista completa de 1.000.000 de elementos, solo un valor a la vez.Ese no sería el caso al usar la versión de lista, donde primero se crearía una lista.

Consulte la sección "Motivación" en PEP 255.

Un uso no obvio de los generadores es la creación de funciones interrumpibles, que le permiten hacer cosas como actualizar la interfaz de usuario o ejecutar varios trabajos "simultáneamente" (intercalados, en realidad) sin utilizar subprocesos.

Encuentro esta explicación que aclara mi duda.Porque existe la posibilidad de que esa persona que no sabe Generators tampoco sé sobre yield

Devolver

La declaración de devolución es donde se destruyen todas las variables locales y el valor resultante se devuelve (devuelve) a la persona que llama.Si se llama a la misma función algún tiempo después, la función obtendrá un nuevo conjunto de variables.

Producir

Pero ¿qué pasa si las variables locales no se descartan cuando salimos de una función?Esto implica que podemos resume the function Donde dejamos.Aquí es donde surge el concepto de generators se introducen y el yield declaración se reanuda donde el function Parado.

  def generate_integers(N):
    for i in xrange(N):
    yield i

    In [1]: gen = generate_integers(3)
    In [2]: gen
    <generator object at 0x8117f90>
    In [3]: gen.next()
    0
    In [4]: gen.next()
    1
    In [5]: gen.next()

Entonces esa es la diferencia entre return y yield declaraciones en Python.

La declaración de rendimiento es lo que convierte a una función en una función generadora.

Entonces los generadores son una herramienta simple y poderosa para crear iteradores.Están escritas como funciones regulares, pero usan el yield declaración cada vez que quieran devolver datos.Cada vez que se llama a next(), el generador continúa donde lo dejó (recuerda todos los valores de datos y qué declaración se ejecutó por última vez).

Ejemplo del mundo real

Digamos que tiene 100 millones de dominios en su tabla MySQL y le gustaría actualizar la clasificación de Alexa para cada dominio.

Lo primero que necesita es seleccionar sus nombres de dominio de la base de datos.

Digamos que el nombre de tu tabla es domains y el nombre de la columna es domain.

Si utiliza SELECT domain FROM domains devolverá 100 millones de filas, lo que consumirá mucha memoria.Entonces su servidor podría fallar.

Entonces decidiste ejecutar el programa en lotes.Digamos que nuestro tamaño de lote es 1000.

En nuestro primer lote consultaremos las primeras 1000 filas, verificaremos la clasificación de Alexa para cada dominio y actualizaremos la fila de la base de datos.

En nuestro segundo lote trabajaremos en las siguientes 1000 filas.En nuestro tercer lote será de 2001 a 3000 y así sucesivamente.

Ahora necesitamos una función generadora que genere nuestros lotes.

Aquí está nuestra función generadora:

def ResultGenerator(cursor, batchsize=1000):
    while True:
        results = cursor.fetchmany(batchsize)
        if not results:
            break
        for result in results:
            yield result

Como puedes ver, nuestra función mantiene yielding los resultados.Si usaste la palabra clave return en lugar de yield, entonces toda la función finalizaría una vez que alcanzara el retorno.

return - returns only once
yield - returns multiple times

Si una función usa la palabra clave yield entonces es un generador.

Ahora puedes iterar así:

db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
    doSomethingWith(result)
db.close()

Almacenamiento en búfer.Cuando es eficiente recuperar datos en porciones grandes, pero procesarlos en porciones pequeñas, entonces un generador podría ayudar:

def bufferedFetch():
  while True:
     buffer = getBigChunkOfData()
     # insert some code to break on 'end of data'
     for i in buffer:    
          yield i

Lo anterior le permite separar fácilmente el almacenamiento en búfer del procesamiento.La función de consumidor ahora puede obtener los valores uno por uno sin preocuparse por el almacenamiento en búfer.

Descubrí que los generadores son muy útiles para limpiar su código y brindarle una forma única de encapsular y modularizar el código.En una situación en la que necesita que algo escupe constantemente valores basados ​​en su propio procesamiento interno y cuando ese algo necesita ser llamado desde cualquier parte de su código (y no solo dentro de un bucle o un bloque, por ejemplo), los generadores son el característica a utilizar.

Un ejemplo abstracto sería un generador de números de Fibonacci que no vive dentro de un bucle y cuando se llama desde cualquier lugar siempre devolverá el siguiente número de la secuencia:

def fib():
    first = 0
    second = 1
    yield first
    yield second

    while 1:
        next = first + second
        yield next
        first = second
        second = next

fibgen1 = fib()
fibgen2 = fib()

Ahora tienes dos objetos generadores de números de Fibonacci a los que puedes llamar desde cualquier lugar de tu código y siempre devolverán números de Fibonacci cada vez más grandes en la secuencia de la siguiente manera:

>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5

Lo bueno de los generadores es que encapsulan el estado sin tener que pasar por los obstáculos de la creación de objetos.Una forma de pensar en ellos es como "funciones" que recuerdan su estado interno.

Obtuve el ejemplo de Fibonacci de Generadores de Python: ¿qué son? y con un poco de imaginación, se pueden idear muchas otras situaciones en las que los generadores constituyen una excelente alternativa a for bucles y otras construcciones de iteración tradicionales.

La explicación sencilla:Considere un for declaración

for item in iterable:
   do_stuff()

Muchas veces, todos los elementos en iterable No es necesario que esté allí desde el principio, pero se puede generar sobre la marcha según sea necesario.Esto puede ser mucho más eficiente en ambos

  • espacio (nunca necesitarás almacenar todos los artículos simultáneamente) y
  • tiempo (la iteración puede finalizar antes de que se necesiten todos los elementos).

Otras veces, ni siquiera conoces todos los elementos de antemano.Por ejemplo:

for command in user_input():
   do_stuff_with(command)

No tienes forma de conocer todos los comandos del usuario de antemano, pero puedes usar un bucle agradable como este si tienes un generador que te entrega los comandos:

def user_input():
    while True:
        wait_for_command()
        cmd = get_command()
        yield cmd

Con los generadores también se puede iterar sobre secuencias infinitas, lo que por supuesto no es posible cuando se itera sobre contenedores.

Mis usos favoritos son las operaciones de "filtrar" y "reducir".

Digamos que estamos leyendo un archivo y solo queremos las líneas que comienzan con "##".

def filter2sharps( aSequence ):
    for l in aSequence:
        if l.startswith("##"):
            yield l

Luego podemos usar la función generadora en un bucle adecuado.

source= file( ... )
for line in filter2sharps( source.readlines() ):
    print line
source.close()

El ejemplo de reducción es similar.Digamos que tenemos un archivo donde necesitamos ubicar bloques de <Location>...</Location> líneas.[No etiquetas HTML, sino líneas que parecen etiquetas.]

def reduceLocation( aSequence ):
    keep= False
    block= None
    for line in aSequence:
        if line.startswith("</Location"):
            block.append( line )
            yield block
            block= None
            keep= False
        elif line.startsWith("<Location"):
            block= [ line ]
            keep= True
        elif keep:
            block.append( line )
        else:
            pass
    if block is not None:
        yield block # A partial block, icky

Nuevamente, podemos usar este generador en un bucle for adecuado.

source = file( ... )
for b in reduceLocation( source.readlines() ):
    print b
source.close()

La idea es que una función generadora nos permita filtrar o reducir una secuencia, produciendo otra secuencia, un valor a la vez.

Un ejemplo práctico en el que podrías utilizar un generador es si tienes algún tipo de forma y quieres iterar sobre sus esquinas, bordes o lo que sea.Para mi propio proyecto (código fuente aquí) Tenía un rectángulo:

class Rect():

    def __init__(self, x, y, width, height):
        self.l_top  = (x, y)
        self.r_top  = (x+width, y)
        self.r_bot  = (x+width, y+height)
        self.l_bot  = (x, y+height)

    def __iter__(self):
        yield self.l_top
        yield self.r_top
        yield self.r_bot
        yield self.l_bot

Ahora puedo crear un rectángulo y recorrer sus esquinas:

myrect=Rect(50, 50, 100, 100)
for corner in myrect:
    print(corner)

En lugar de __iter__ podrías tener un método iter_corners y llama eso con for corner in myrect.iter_corners().Es simplemente más elegante de usar. __iter__ desde entonces podemos usar el nombre de la instancia de clase directamente en el for expresión.

Básicamente, evitar funciones de devolución de llamada al iterar sobre el estado de mantenimiento de la entrada.

Ver aquí y aquí para obtener una descripción general de lo que se puede hacer usando generadores.

Algunas buenas respuestas aquí, sin embargo, también recomendaría una lectura completa de Python. Tutorial de programación funcional lo que ayuda a explicar algunos de los casos de uso más potentes de los generadores.

Utilizo generadores cuando nuestro servidor web actúa como proxy:

  1. El cliente solicita una URL proxy del servidor.
  2. El servidor comienza a cargar la URL de destino.
  3. El servidor cede para devolver los resultados al cliente tan pronto como los reciba.

Dado que no se ha mencionado el método de envío de un generador, aquí hay un ejemplo:

def test():
    for i in xrange(5):
        val = yield
        print(val)

t = test()

# Proceed to 'yield' statement
next(t)

# Send value to yield
t.send(1)
t.send('2')
t.send([3])

Muestra la posibilidad de enviar un valor a un generador en funcionamiento.Un curso más avanzado sobre generadores en el video a continuación (que incluye yield desde la explicación, generadores para procesamiento paralelo, escape del límite de recursividad, etc.)

David Beazley sobre generadores en PyCon 2014

Montones de cosas.Cada vez que desee generar una secuencia de elementos, pero no quiera tener que "materializarlos" todos en una lista a la vez.Por ejemplo, podrías tener un generador simple que devuelva números primos:

def primes():
    primes_found = set()
    primes_found.add(2)
    yield 2
    for i in itertools.count(1):
        candidate = i * 2 + 1
        if not all(candidate % prime for prime in primes_found):
            primes_found.add(candidate)
            yield candidate

Luego podrías usarlo para generar los productos de números primos posteriores:

def prime_products():
    primeiter = primes()
    prev = primeiter.next()
    for prime in primeiter:
        yield prime * prev
        prev = prime

Estos son ejemplos bastante triviales, pero puede ver cómo puede resultar útil para procesar conjuntos de datos grandes (¡potencialmente infinitos!) sin generarlos por adelantado, que es sólo uno de los usos más obvios.

También es bueno para imprimir números primos hasta n:

def genprime(n=10):
    for num in range(3, n+1):
        for factor in range(2, num):
            if num%factor == 0:
                break
        else:
            yield(num)

for prime_num in genprime(100):
    print(prime_num)
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top