¿Cuál es la diferencia entre el rendimiento y el rendimiento en Python 3.3.2+?

Después de Python 3.3.2+, Python admite una nueva syntax para crear una función de generador.

yield from  

He hecho un bash rápido para esto por

 >>> def g(): ... yield from [1,2,3,4] ... >>> for i in g(): ... print(i) ... 1 2 3 4 >>> 

Parece simple de usar pero el documento PEP es complejo. Mi pregunta es ¿hay alguna otra diferencia en comparación con la statement de rendimiento anterior? Gracias.

Para la mayoría de las aplicaciones, el yield from solo produce todo desde la izquierda iterable en orden:

 def iterable1(): yield 1 yield 2 def iterable2(): yield from iterable1() yield 3 assert list(iterable2) == [1, 2, 3] 

Para el 90% de los usuarios que ven esta publicación, supongo que esto será una explicación suficiente para ellos. yield from delegates a lo iterable en el lado derecho.


Coroutines

Sin embargo, hay algunas circunstancias generadoras más esotéricas que también tienen importancia aquí. Un hecho menos conocido acerca de los generadores es que se pueden utilizar como co-rutinas. Esto no es muy común, pero puede enviar datos a un generador si desea:

 def coroutine(): x = yield None yield 'You sent: %s' % x c = coroutine() next(c) print(c.send('Hello world')) 

Aparte: puede que te preguntes cuál es el caso de uso para esto (y no estás solo). Un ejemplo es el decorador contextlib.contextmanager . Las co-rutinas también pueden usarse para paralelizar ciertas tareas. No conozco muchos lugares donde se aproveche esto, pero la API del almacén de datos ndb google app-engine lo usa para operaciones asíncronas de una manera bastante ingeniosa.

Ahora, supongamos que send datos a un generador que está generando datos de otro generador … ¿Cómo se notifica al generador original? La respuesta es que no en python2.x, donde necesita envolver el generador usted mismo:

 def python2_generator_wapper(): for item in some_wrapped_generator(): yield item 

Al menos no sin mucho dolor.

 def python2_coroutine_wrapper(): """This doesn't work. Somebody smarter than me needs to fix it. . . Pain. Misery. Death lurks here :-(""" # See https://www.python.org/dev/peps/pep-0380/#formal-semantics for actual working implementation :-) g = some_wrapped_generator() for item in g: try: val = yield item except Exception as forward_exception: # What exceptions should I not catch again? g.throw(forward_exception) else: if val is not None: g.send(val) # Oops, we just consumed another cycle of g ... How do we handle that properly ... 

Todo esto se vuelve trivial con el yield from :

 def coroutine_wrapper(): yield from coroutine() 

Porque yield from verdaderos delegates ( ¡todo! ) Al generador subyacente.


Semántica de retorno

Tenga en cuenta que el PEP en cuestión también cambia la semántica de retorno. Si bien no está directamente en la pregunta de OP, vale la pena una rápida digresión si está dispuesto a hacerlo. En python2.x, no puedes hacer lo siguiente:

 def iterable(): yield 'foo' return 'done' 

Es un SyntaxError . Con la actualización a yield , la función anterior no es legal. Nuevamente, el caso de uso primario es con las coroutinas (ver arriba). Puede enviar datos al generador y puede hacer su trabajo de forma mágica (¿quizás utilizando subprocesos?) Mientras que el rest del progtwig hace otras cosas. Cuando el control de flujo pasa de vuelta al generador, StopIteration se StopIteration (como es normal para el final de un generador), pero ahora StopIteration tendrá una carga útil de datos. Es lo mismo que si un progtwigdor escribiera:

  raise StopIteration('done') 

Ahora la persona que llama puede capturar esa excepción y hacer algo con la carga de datos para beneficiar al rest de la humanidad.

A primera vista, el yield from es un atajo algorítmico para:

 def generator1(): for item in generator2(): yield item # do more things in this generator 

Que es entonces en su mayoría equivalente a solo:

 def generator1(): yield from generator2() # more things on this generator 

En inglés: cuando se utiliza dentro de un iterable, el yield from cada elemento en otro iterable, como si ese elemento procediera del primer generador, desde el punto de vista del código que llama al primer generador.

El principal razonamiento para su creación es permitir una refactorización fácil del código que depende en gran medida de los iteradores; el código que utiliza funciones ordinarias siempre podría tener, a muy poco costo adicional, bloques de una función refactorizados a otras funciones, que luego se denominan, que divide las tareas , simplifica la lectura y el mantenimiento del código y permite una mayor reutilización de fragmentos de código pequeños –

Así, grandes funciones como esta:

 def func1(): # some calculation for i in somesequence: # complex calculation using i # ... # ... # ... # some more code to wrap up results # finalizing # ... 

Puede convertirse en código así, sin inconvenientes:

 def func2(i): # complex calculation using i # ... # ... # ... return calculated_value def func1(): # some calculation for i in somesequence: func2(i) # some more code to wrap up results # finalizing # ... 

Al llegar a los iteradores sin embargo, el formulario

 def generator1(): for item in generator2(): yield item # do more things in this generator for item in generator1(): # do things 

requiere que para cada elemento consumido del generator2 , el contexto en ejecución se cambie primero al generator1 , no se haga nada en ese contexto, y el cotnext se deba cambiar a generator2 – y cuando ese produce un valor, hay otro interruptor de contexto intermedio para generator1, antes de obtener el valor del código real que consume esos valores.

Con el rendimiento de estos cambios de contexto intermedio se evitan, lo que puede ahorrar bastante recursos si hay muchos iteradores encadenados: el contexto cambia directamente del contexto que consume el generador más externo al generador más interno, omitiendo el contexto de los generadores intermedios en conjunto. Hasta que los interiores se agoten.

Posteriormente, el lenguaje aprovechó este “tunelling” a través de contextos intermedios para usar estos generadores como co-rutinas: funciones que pueden realizar llamadas asíncronas. Con el marco adecuado en su lugar, tal como se describe en https://www.python.org/dev/peps/pep-3156/ , estas co-rutinas se escriben de manera que cuando llamen a una función que lleve mucho tiempo tiempo de resolución (debido a una operación de la red, o una operación intensiva de la CPU que se puede descargar a otro subproceso) – esa llamada se realiza con un yield from statement – el bucle principal del marco se organiza para que la función costosa llamada se programe correctamente, y retoma la ejecución (el mainloop del framework es siempre el código que llama a las co-rutinas en sí). Cuando el resultado costoso está listo, el marco hace que la co-rutina llamada se comporte como un generador agotado, y se reanuda la ejecución de la primera co-rutina.

Desde el punto de vista del progtwigdor, es como si el código se ejecutara de manera directa, sin interrupciones. Desde el punto de vista del proceso, la co-rutina se detuvo en el punto de la llamada costosa, y otras (posiblemente llamadas paralelas a la misma co-rutina) continuaron ejecutándose.

Entonces, uno podría escribir como parte de un rastreador web algún código a lo largo:

 @asyncio.coroutine def crawler(url): page_content = yield from async_http_fetch(url) urls = parse(page_content) ... 

Lo que podría recuperar decenas de páginas html al mismo tiempo cuando se llama desde el bucle asyncio.

Python 3.4 agregó el módulo asyncio a stdlib como el proveedor predeterminado para este tipo de funcionalidad. Funcionó tan bien, que en Python 3.5 se agregaron varias palabras clave nuevas al lenguaje para distinguir co-rutinas y llamadas asíncronas del uso del generador, descrito anteriormente. Estos se describen en https://www.python.org/dev/peps/pep-0492/

Aquí hay un ejemplo que lo ilustra:

 >>> def g(): ... yield from range(5) ... >>> list(g()) [0, 1, 2, 3, 4] >>> def g(): ... yield range(5) ... >>> list(g()) [range(0, 5)] >>> 

yield from rendimientos de cada ítem de lo iterable, pero el yield es el mismo iterable.