Conversión de la statement de “rendimiento desde” a código de Python 2.7

Tenía un código a continuación en Python 3.2 y quería ejecutarlo en Python 2.7. Lo convertí (he puesto el código de missing_elements en ambas versiones) pero no estoy seguro de si esa es la forma más eficiente de hacerlo. Básicamente, ¿qué sucede si hay dos yield from llamadas como abajo en la mitad superior y en la mitad inferior en la función missing_element ? ¿Las entradas de las dos mitades (superior e inferior) se adjuntan en una lista para que la recursión principal funcione con el yield from llamada y use ambas mitades juntas?

 def missing_elements(L, start, end): # Python 3.2 if end - start  1: yield from range(L[start] + 1, L[end]) return index = start + (end - start) // 2 # is the lower half consecutive? consecutive_low = L[index] == L[start] + (index - start) if not consecutive_low: yield from missing_elements(L, start, index) # is the upper part consecutive? consecutive_high = L[index] == L[end] - (end - index) if not consecutive_high: yield from missing_elements(L, index, end) def main(): L = [10, 11, 13, 14, 15, 16, 17, 18, 20] print(list(missing_elements(L, 0, len(L)-1))) L = range(10, 21) print(list(missing_elements(L, 0, len(L)-1))) def missing_elements(L, start, end): # Python 2.7 return_list = [] if end - start  1: return range(L[start] + 1, L[end]) index = start + (end - start) // 2 # is the lower half consecutive? consecutive_low = L[index] == L[start] + (index - start) if not consecutive_low: return_list.append(missing_elements(L, start, index)) # is the upper part consecutive? consecutive_high = L[index] == L[end] - (end - index) if not consecutive_high: return_list.append(missing_elements(L, index, end)) return return_list 

Si no usa los resultados de sus rendimientos, * siempre puede activar esto:

 yield from foo 

… dentro de esto:

 for bar in foo: yield bar 

Puede haber un costo de rendimiento, ** pero nunca hay una diferencia semántica.


¿Las entradas de las dos mitades (superior e inferior) se adjuntan en una lista para que la recursión principal funcione con el rendimiento de la llamada y use ambas mitades juntas?

¡No! El objective principal de los iteradores y generadores es que no se crean listas reales y no se agregan juntas.

Pero el efecto es similar: solo cedes de uno, luego cedes de otro.

Si piensa en la mitad superior y en la inferior como “listas perezosas”, entonces sí, puede pensar en esto como un “anexo perezoso” que crea una “lista perezosa” más grande. Y si llama a la list en el resultado de la función principal, por supuesto obtendrá una list real que es equivalente a agregar las dos listas que habría obtenido si hubiera hecho la yield list(…) lugar del yield from …

Pero creo que es más fácil pensarlo al revés: lo que hace es exactamente lo mismo que los bucles for .

Si guardó los dos iteradores en variables y realizó un bucle en itertools.chain(upper, lower) , sería lo mismo que un bucle en el primero y luego un bucle en el segundo, ¿verdad? No hay diferencia aquí. De hecho, podría implementar la chain como simplemente:

 for arg in *args: yield from arg 

* No son los valores que el generador cede a su llamador, el valor de las expresiones de rendimiento en sí mismas, dentro del generador (que proviene del llamante que utiliza el método de send ), como se describe en PEP 342 . No estás usando estos en tus ejemplos. Y estoy dispuesto a apostar que no estás en tu código real. Pero el código de estilo coroutine a menudo usa el valor de un yield from expresión; consulte PEP 3156 para ver ejemplos. Dicho código generalmente depende de otras características de los generadores de Python 3.3, en particular, el nuevo valor StopIteration.value del mismo PEP 380 que introdujo el yield from , por lo que tendrá que ser reescrito. Pero si no, puede usar el PEP. También le muestra el horroroso equivalente completo y, por supuesto, puede reducir las partes que no le interesan. Y si no usa el valor de la expresión, se reduce a las dos líneas de arriba.

** No es enorme, y no hay nada que puedas hacer al respecto, aparte de usar Python 3.3 o reestructurar completamente tu código. Es exactamente el mismo caso que la traducción de listas de comprensión a los ciclos de Python 1.5, o cualquier otro caso cuando hay una nueva optimización en la versión XY y necesita usar una versión anterior.

Reemplazarlos con bucles for:

 yield from range(L[start] + 1, L[end]) ==> for i in range(L[start] + 1, L[end]): yield i 

Lo mismo sobre los elementos:

 yield from missing_elements(L, index, end) ==> for el in missing_elements(L, index, end): yield el 

Acabo de encontrar este problema y mi uso fue un poco más difícil ya que necesitaba el valor de retorno del yield from :

 result = yield from other_gen() 

Esto no se puede representar como un simple bucle for pero se puede reproducir con esto:

 _iter = iter(other_gen()) try: while True: #broken by StopIteration yield next(_iter) except StopIteration as e: if e.args: result = e.args[0] else: result = None 

Esperemos que esto ayude a las personas que encuentran el mismo problema. 🙂

Creo que encontré una manera de emular el yield from Python 3.x yield from construcción en Python 2.x. No es eficiente y es un poco hacky, pero aquí está:

 import types def inline_generators(fn): def inline(value): if isinstance(value, InlineGenerator): for x in value.wrapped: for y in inline(x): yield y else: yield value def wrapped(*args, **kwargs): result = fn(*args, **kwargs) if isinstance(result, types.GeneratorType): result = inline(_from(result)) return result return wrapped class InlineGenerator(object): def __init__(self, wrapped): self.wrapped = wrapped def _from(value): assert isinstance(value, types.GeneratorType) return InlineGenerator(value) 

Uso:

 @inline_generators def outer(x): def inner_inner(x): for x in range(1, x + 1): yield x def inner(x): for x in range(1, x + 1): yield _from(inner_inner(x)) for x in range(1, x + 1): yield _from(inner(x)) for x in outer(3): print x, 

Produce salida:

 1 1 1 2 1 1 2 1 2 3 

Tal vez alguien encuentra esto útil.

Problemas conocidos: falta compatibilidad con send () y varios casos de esquinas descritos en PEP 380. Se podrían agregar y editaré mi entrada una vez que la haga funcionar.

¿Qué hay de usar la definición de pep-380 para construir una versión de syntax de Python 2?

La statement:

 RESULT = yield from EXPR 

es semánticamente equivalente a:

 _i = iter(EXPR) try: _y = next(_i) except StopIteration as _e: _r = _e.value else: while 1: try: _s = yield _y except GeneratorExit as _e: try: _m = _i.close except AttributeError: pass else: _m() raise _e except BaseException as _e: _x = sys.exc_info() try: _m = _i.throw except AttributeError: raise _e else: try: _y = _m(*_x) except StopIteration as _e: _r = _e.value break else: try: if _s is None: _y = next(_i) else: _y = _i.send(_s) except StopIteration as _e: _r = _e.value break RESULT = _r 

En un generador, la statement:

 return value 

es semánticamente equivalente a

 raise StopIteration(value) 

excepto que, como en la actualidad, la excepción no puede ser detectada por cláusulas de except dentro del generador de retorno.

La excepción StopIteration se comporta como si se definiera así:

 class StopIteration(Exception): def __init__(self, *args): if len(args) > 0: self.value = args[0] else: self.value = None Exception.__init__(self, *args) 

Descubrí que usar contextos de recursos (usar el módulo de recursos de Python ) es un mecanismo elegante para implementar subgeneradores en Python 2.7. Convenientemente ya había estado usando los contextos de recursos de todos modos.

Si en Python 3.3 tendrías:

 @resources.register_func def get_a_thing(type_of_thing): if type_of_thing is "A": yield from complicated_logic_for_handling_a() else: yield from complicated_logic_for_handling_b() def complicated_logic_for_handling_a(): a = expensive_setup_for_a() yield a expensive_tear_down_for_a() def complicated_logic_for_handling_b(): b = expensive_setup_for_b() yield b expensive_tear_down_for_b() 

En Python 2.7 tendrías:

 @resources.register_func def get_a_thing(type_of_thing): if type_of_thing is "A": with resources.complicated_logic_for_handling_a_ctx() as a: yield a else: with resources.complicated_logic_for_handling_b_ctx() as b: yield b @resources.register_func def complicated_logic_for_handling_a(): a = expensive_setup_for_a() yield a expensive_tear_down_for_a() @resources.register_func def complicated_logic_for_handling_b(): b = expensive_setup_for_b() yield b expensive_tear_down_for_b() 

Observe cómo las operaciones de lógica complicada solo requieren el registro como un recurso.