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.
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.
Respuestas:
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)
yield
y join
despué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 yield
hasta 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.
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.
list(fibon(5))
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.
Encuentro esta explicación que aclara mis dudas. Porque existe la posibilidad de que la persona que no sabe Generators
tampoco 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 function
donde lo dejamos. Aquí es donde generators
se introduce el concepto de y la yield
declaración se reanuda donde la function
dejó.
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 return
yyield
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 yield
declaració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).
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 domains
y 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 yield
los resultados. Si utilizó la palabra clave en return
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()
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.
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 for
bucles y otras construcciones de iteración tradicionales.
La explicación simple: considere una for
declaración
for item in iterable:
do_stuff()
La mayoría de las veces, todos los elementos iterable
no 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
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.
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.
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.
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_corners
y 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 for
expresión.
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.
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 yield
desde la expiración, generadores para procesamiento paralelo, escapar del límite de recursión, etc.)
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.
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)