¿Para qué puedes usar las funciones del generador Python?

213

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

quamrana
fuente
66
quizás una mejor pregunta sería cuando no deberíamos usarlos
cregox
1
Ejemplo del mundo real aquí
Giri

Respuestas:

239

Los generadores le 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 itera. 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 de generador se detiene 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 los que no sabe si va a necesitar todos los resultados, o donde no desea asignar la memoria para todos los resultados al mismo tiempo . O para situaciones en las que el generador usa otro generador, o consume algún otro recurso, y es más conveniente si eso sucedió lo más tarde posible.

Otro uso para los generadores (que es realmente el mismo) es reemplazar las devoluciones de llamada con iteración. 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 periódicamente llamará a esta devolución de llamada. El enfoque del generador es que la función de trabajo (ahora un generador) no sabe nada acerca de 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, supongamos que escribió un programa de 'búsqueda de sistema de archivos'. Puede 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 sería más eficiente en cuanto a memoria y mucho más amigable con 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 haciendo que la función de búsqueda sea un generador e iterando sobre el resultado.

Si desea ver un ejemplo de los dos últimos enfoques, vea os.path.walk () (la antigua función de caminar del sistema de archivos con devolución de llamada) y os.walk () (el nuevo generador de caminar del sistema de archivos). Por supuesto, si Realmente quería recopilar todos los resultados en una lista, el enfoque del generador es trivial para convertir al enfoque de la lista grande:

big_list = list(the_generator)
Thomas Wouters
fuente
¿Un generador como el que produce listas de sistemas de archivos realiza acciones en paralelo al código que ejecuta ese generador en un bucle? Idealmente, la computadora ejecutaría el cuerpo del bucle (procesando el último resultado) mientras hacía simultáneamente todo lo que el generador debe hacer para obtener el siguiente valor.
Steven Lu
@StevenLu: a menos que se tome la molestia de lanzar subprocesos manualmente antes yieldy joindespués para obtener el siguiente resultado, no se ejecuta en paralelo (y ningún generador de biblioteca estándar hace esto; lanzar subprocesos en secreto está mal visto). El generador se detiene en cada uno yieldhasta que se solicita el siguiente valor. Si el generador está envolviendo E / S, el sistema operativo podría estar almacenando en caché de forma proactiva los datos del archivo, suponiendo que se solicitará en breve, pero ese es el sistema operativo, Python no está involucrado.
ShadowRanger 01 de
90

Una de las razones para usar el generador es hacer que la solución sea más clara para algún tipo de solución.

El otro es tratar los resultados de uno en uno, evitando crear enormes listas de resultados que de todos modos procesaría separados.

Si tiene una función fibonacci-up-to-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

Puede escribir más fácilmente la función de esta manera:

# 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 usa la función de esta manera:

for x in fibon(1000000):
    print x,

en este ejemplo, si usa la versión del generador, no se creará la lista completa de artículos 1000000, solo un valor a la vez. Ese no sería el caso cuando se usa la versión de la lista, donde primero se crearía una lista.

nosklo
fuente
18
y si necesita una lista, siempre puede hacerlolist(fibon(5))
endolito el
41

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

Nickolay
fuente
1
La sección Motivación es agradable porque tiene un ejemplo específico: "Cuando una función de productor tiene un trabajo lo suficientemente difícil como para mantener el estado entre los valores producidos, la mayoría de los lenguajes de programación no ofrecen una solución agradable y eficiente más allá de agregar una función de devolución de llamada al argumento del productor lista ... Por ejemplo, tokenize.py en la biblioteca estándar adopta este enfoque "
Ben Creasy
38

Encuentro esta explicación que aclara mis dudas. Porque existe la posibilidad de que la persona que no sabe Generatorstampoco sepa sobreyield

Regreso

La declaración de devolución es donde todas las variables locales se destruyen 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 nuevas.

rendimiento

Pero, ¿qué pasa si las variables locales no se desechan cuando salimos de una función? Esto implica que podemos resume the functiondonde lo dejamos. Aquí es donde generatorsse introduce el concepto de y la yielddeclaración se reanuda donde la functiondejó.

  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()

Esa es la diferencia entre returnyyield declaraciones en Python.

La declaración de rendimiento es lo que hace que una función sea una función generadora.

Por lo tanto, los generadores son una herramienta simple y poderosa para crear iteradores. Se escriben como funciones regulares, pero usan la yielddeclaración cada vez que desean devolver datos. Cada vez que se llama a next (), el generador reanuda donde lo dejó (recuerda todos los valores de datos y qué declaración se ejecutó por última vez).

Espejismo
fuente
33

Ejemplo del mundo real

Supongamos que tiene 100 millones de dominios en su tabla MySQL y desea actualizar el rango 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 su tabla es domainsy el nombre de la columna es domain.

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

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

En nuestro primer lote, consultaremos las primeras 1000 filas, verificaremos el rango 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 de generador:

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

Como puede ver, nuestra función mantiene yieldlos resultados. Si utilizó la palabra clave en returnlugar 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()
Giri
fuente
¡Sería más práctico si el rendimiento pudiera explicarse en términos de programación recursiva / dinámica!
igaurav
27

Buffering. Cuando es eficiente obtener datos en fragmentos grandes, pero procesarlos en fragmentos pequeños, 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.

Rafał Dowgird
fuente
3
Si getBigChuckOfData no es perezoso, entonces no entiendo qué beneficio tiene el rendimiento aquí. ¿Cuál es un caso de uso para esta función?
Sean Geoffrey Pietz
1
Pero el punto es que, IIUC, bufferedFetch está demorando la llamada a getBigChunkOfData. Si getBigChunkOfData ya era vago, entonces bufferedFetch sería inútil. Cada llamada a bufferedFetch () devolverá un elemento de búfer, a pesar de que ya se haya leído un BigChunk. Y no es necesario que cuente explícitamente el siguiente elemento para devolver, porque la mecánica del rendimiento lo hace implícitamente.
hmijail llora a los representantes el
21

He descubierto que los generadores son muy útiles para limpiar su código y al brindarle una forma única de encapsular y modularizar el código. En una situación en la que necesita algo para escupir 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 la característica para 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 en 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 tiene dos objetos generadores de números de Fibonacci a los que puede llamar desde cualquier parte de su código y siempre devolverán números de Fibonacci cada vez más grandes en 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 aros de la creación de objetos. Una forma de pensar en ellas es como "funciones" que recuerdan su estado interno.

Obtuve el ejemplo de Fibonacci de Python Generators. ¿Qué son? y con un poco de imaginación, puede encontrar muchas otras situaciones en las que los generadores son una excelente alternativa a los forbucles y otras construcciones de iteración tradicionales.

Andz
fuente
19

La explicación simple: considere una fordeclaración

for item in iterable:
   do_stuff()

La mayoría de las veces, todos los elementos iterableno necesitan estar allí desde el principio, sino que se pueden generar sobre la marcha según sea necesario. Esto puede ser mucho más eficiente en ambos

  • espacio (nunca necesita 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 conoce todos los elementos con anticipación. Por ejemplo:

for command in user_input():
   do_stuff_with(command)

No tiene forma de conocer todos los comandos del usuario de antemano, pero puede usar un buen ciclo como este si tiene un generador que le entrega los comandos:

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

Con los generadores también puede tener iteración sobre secuencias infinitas, lo que, por supuesto, no es posible al iterar sobre contenedores.

dF.
fuente
... y se podría generar una secuencia infinita al recorrer repetidamente una lista pequeña y volver al principio una vez que se alcanza el final. Lo uso para seleccionar colores en gráficos, o producir vibrantes o hilanderos en el texto.
Andrej Panjkov
@mataap: Hay un itertoolpara eso, mira cycles.
Martineau
12

Mis usos favoritos son las operaciones de "filtro" y "reducción".

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

Entonces podemos usar la función de generador en un bucle apropiado

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 son 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 apropiado para.

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

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

S.Lott
fuente
8
fileobj.readlines()leería todo el archivo en una lista en la memoria, frustrando el propósito de usar generadores. Como los objetos de archivo ya son iterables, puede usarlos for b in your_generator(fileobject):en su lugar. De esa manera, su archivo se leerá una línea a la vez, para evitar leer el archivo completo.
nosklo
reduceLocation es bastante extraño produciendo una lista, ¿por qué no solo ceder cada línea? También filtrar y reducir son componentes incorporados con comportamientos esperados (consulte la ayuda en ipython, etc.), su uso de "reducir" es el mismo que el filtro.
James Antill
Buen punto en las líneas de lectura (). Por lo general, me doy cuenta de que los archivos son iteradores de línea de primera clase durante las pruebas unitarias.
S.Lott
En realidad, la "reducción" combina múltiples líneas individuales en un objeto compuesto. De acuerdo, es una lista, pero sigue siendo una reducción tomada de la fuente.
S.Lott
9

Un ejemplo práctico en el que podría utilizar un generador es si tiene algún tipo de forma y desea 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__usted podría tener un método iter_cornersy llamarlo con for corner in myrect.iter_corners(). Es más elegante de usar __iter__ya que podemos usar el nombre de instancia de clase directamente en la forexpresión.

Pithikos
fuente
Adoré la idea de pasar campos de clase similares como generador
eusoubrasileiro
7

Básicamente evitando las funciones de devolución de llamada cuando se itera sobre el estado de mantenimiento de entrada.

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

MvdD
fuente
4

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

Songololo
fuente
3

Como 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 ejecución. Un curso más avanzado sobre generadores en el video a continuación (que incluye yielddesde la expiración, generadores para procesamiento paralelo, escapar del límite de recursión, etc.)

David Beazley en generadores en PyCon 2014

John Damen
fuente
2

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

  1. El cliente solicita una URL del servidor proxy
  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.
Brian
fuente
1

Montones de cosas. Cada vez que desee generar una secuencia de elementos, pero no desee tener que 'materializarlos' en una lista a la vez. Por ejemplo, podría 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 puede usar eso para generar los productos de 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 ser útil para procesar grandes conjuntos de datos (¡potencialmente infinitos!) Sin generarlos de antemano, que es solo uno de los usos más obvios.

Nick Johnson
fuente
si no hay ninguno (candidato% primo para primo en primes_found) debe ser si todos (candidato% primo para primo en primes_found)
rjmunro
Sí, lo que significa que escribir "si no hay (candidato% primer == 0 para privilegiada en primes_found) El suyo es un poco más ordenado, aunque :)..
Nick Johnson
Supongo que olvidó eliminar el 'no' de si no todos (candidato% prime para prime en primes_found)
Thava
0

También es bueno para imprimir los 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)
Sébastien Wieckowski
fuente