iteradores - ¿Para qué puedes usar las funciones del generador de Python?
que es un iterable en python (16)
Estoy empezando a aprender Python y me he encontrado con funciones de generador, aquellas que tienen una declaración de rendimiento en ellas. Quiero saber qué tipos de problemas son realmente buenos para resolver estas funciones.
Ejemplo del mundo real
Digamos que tiene 100 millones de dominios en su tabla de MySQL, y le gustaría actualizar el rango de Alexa para cada dominio.
Lo primero que necesitas es seleccionar tus nombres de dominio de la base de datos.
Digamos que el nombre de la tabla es domains
y el nombre de la columna es domain
.
Si usa el SELECT domain FROM domains
, devolverá 100 millones de filas, lo que consumirá mucha memoria. Por lo que su servidor podría fallar.
Así que 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 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 de generador 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 sigue dando los resultados. Si usó el return
la palabra clave en lugar del yield
, la función completa se finalizaría una vez que alcanzara el retorno.
return - returns only once
yield - returns multiple times
Si una función utiliza el yield
la palabra clave, 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()
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.
- Particularmente interesante es que ahora es posible actualizar la variable de rendimiento desde fuera de la función del generador , por lo que es posible crear corrutinas dinámicas e interconectadas con relativamente poco esfuerzo.
- También vea PEP 342: Coroutines a través de Generadores Mejorados para más información.
Buffering. Cuando es eficiente obtener datos en grandes porciones, pero procesarlos en pequeñas porciones, 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 búfer del procesamiento. La función del consumidor ahora puede obtener los valores uno por uno sin preocuparse por el almacenamiento en búfer.
Como el método de envío de un generador no se ha mencionado, 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 los generadores en el video a continuación (incluido el yield
de la exploración, los generadores para el procesamiento en paralelo, el escape del límite de recursión, etc.)
Descubrí que los generadores son muy útiles para limpiar su código y al brindarle una manera única de encapsular y modular el código. En una situación en la que necesita algo para escupir constantemente valores en función de su propio procesamiento interno y cuando se debe llamar a ese elemento desde cualquier lugar 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 crear objetos. Una forma de pensar acerca de ellos es como "funciones" que recuerdan su estado interno.
Obtuve el ejemplo de Fibonacci de los generadores de Python: ¿Qué son? y con un poco de imaginación, puede crear muchas otras situaciones en las que los generadores son una excelente alternativa for
bucles y otras construcciones de iteración tradicionales.
Encuentro esta explicación que aclara mi duda. Porque existe la posibilidad de que la persona que no sabe Generators
tampoco sepa sobre yield
Regreso
La declaración de retorno es donde se destruyen todas las variables locales y el valor resultante se devuelve (devuelve) al llamante. Si se llama a la misma función algún tiempo después, la función obtendrá un nuevo conjunto de variables.
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 la dejamos. Aquí es donde se introduce el concepto de generators
y la declaración de yield
reanuda donde se detuvo la function
.
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 las declaraciones de yield
y yield
en Python.
La declaración de rendimiento es lo que hace que una función sea una función generadora.
Así que los generadores son una herramienta simple y poderosa para crear iteradores. Se escriben como funciones regulares, pero usan la declaración de yield
cuando quieren devolver datos. Cada vez que se llama a next (), el generador se reanuda donde lo dejó (recuerda todos los valores de datos y la última instrucción que se ejecutó).
La explicación simple: considerar una declaración for
for item in iterable:
do_stuff()
La mayoría de las veces, no es necesario que todos los elementos en iterable
estén allí desde el principio, sino que se pueden generar sobre la marcha según sea necesario. Esto puede ser mucho más eficiente tanto en
- 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 sabes todos los elementos antes de tiempo. Por ejemplo:
for command in user_input():
do_stuff_with(command)
No tiene forma de saber de antemano todos los comandos del usuario, pero puede usar un buen bucle como este si tiene un generador que le da 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 cuando se itera sobre contenedores.
Los generadores te dan perezosa evaluación. Los usas iterando sobre ellos, ya sea explícitamente con ''for'' o implícitamente pasándolos 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 de una vez, los devuelven uno por uno, y la función del 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 sí mismos) 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 utiliza 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 en realidad es 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, se usaría una función de devolución de llamada para esto. Pasa esta devolución de llamada a la función de trabajo y llamaría 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 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 por separado y pasarla a la función de trabajo, hace todo el trabajo de informes en un pequeño ''for'' loop alrededor del generador.
Por ejemplo, digamos que escribiste un programa de ''búsqueda de sistema de archivos''. Puede realizar la búsqueda en su totalidad, recopilar los resultados y luego mostrarlos uno por uno. 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 puede mostrar los resultados mientras los encuentra, lo que sería más eficiente en cuanto a la memoria y mucho más amigable para el usuario. Esto último se podría hacer pasando la función de impresión de resultados a la función de búsqueda del sistema de archivos, o se podría hacer simplemente haciendo de la función de búsqueda un generador e iterando el resultado.
Si desea ver un ejemplo de los dos últimos enfoques, vea os.path.walk () (la antigua función de desplazamiento del sistema de archivos con devolución de llamada) y os.walk () (el nuevo generador de desplazamiento 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 gran lista:
big_list = list(the_generator)
Mis usos favoritos son "filtrar" y "reducir" las operaciones.
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 del 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 líneas <Location>...</Location>
. [No son etiquetas HTML, sino líneas que parecen tener el mismo aspecto que una etiqueta].
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.
source = file( ... )
for b in reduceLocation( source.readlines() ):
print b
source.close()
La idea es que una función de generador nos permita filtrar o reducir una secuencia, produciendo otra secuencia, un valor a la vez.
Montones de cosas Cada vez que quiera generar una secuencia de elementos, pero no quiera 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
Entonces podrías 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)
Un ejemplo práctico en el que podría hacer uso de un generador es si tiene algún tipo de forma y desea recorrer sus esquinas, bordes o lo que sea. Para mi propio proyecto (código fuente here ) tuve 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 hacer un bucle sobre sus esquinas:
myrect=Rect(50, 50, 100, 100)
for corner in myrect:
print(corner)
En lugar de __iter__
puede tener un método iter_corners
y llamar a for corner in myrect.iter_corners()
. Es más elegante usar __iter__
ya que entonces podemos usar el nombre de la instancia de la clase directamente en la expresión for
.
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.
La otra es tratar los resultados de uno en uno, evitando la creación de grandes listas de resultados que de todos modos procesaría por separado.
Si tiene una función de 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 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 utiliza la versión del generador, la lista completa de elementos 1000000 no se creará en absoluto, solo un valor a la vez. Ese no sería el caso cuando se usa la versión de lista, donde primero se creará una lista.
Uso generadores cuando nuestro servidor web actúa como proxy:
- El cliente solicita un url proxy del servidor
- El servidor comienza a cargar la URL de destino.
- El servidor cede para devolver los resultados al cliente tan pronto como los obtiene.
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 usar subprocesos.