Alternando entre iteradores en Python

¿Cuál es la forma más eficiente de alternar valores de toma de diferentes iteradores en Python, de modo que, por ejemplo, alternate(xrange(1, 7, 2), xrange(2, 8, 2)) produzca 1, 2, 3, 4, 5, 6. Sé que una forma de implementarlo sería:

 def alternate(*iters): while True: for i in iters: try: yield i.next() except StopIteration: pass 

¿Pero hay una forma más eficiente o más limpia? (O, mejor aún, ¿una función de itertools que extrañé?)

que tal zip También puedes probar el izip de itertools.

 >>> zip(xrange(1, 7, 2),xrange(2, 8 , 2)) [(1, 2), (3, 4), (5, 6)] 

Si esto no es lo que desea, dé más ejemplos en la publicación de su pregunta.

Para una implementación “limpia”, desea

 itertools.chain(*itertools.izip(*iters)) 

pero tal vez quieras

 itertools.chain(*itertools.izip_longest(*iters)) 

Ver roundrobin en la itertools “Recetas” de itertools . Es una versión más general de suplente.

 def roundrobin(*iterables): "roundrobin('ABC', 'D', 'EF') --> ADEBFC" # Recipe credited to George Sakkis pending = len(iterables) nexts = cycle(iter(it).__next__ for it in iterables) while pending: try: for next in nexts: yield next() except StopIteration: pending -= 1 nexts = cycle(islice(nexts, pending)) 

Podrías definir alternate como esta:

 import itertools def alternate(*iters): for elt in itertools.chain.from_iterable( itertools.izip(*iters)): yield elt print list(alternate(xrange(1, 7, 2), xrange(2, 8, 2))) 

Esto deja abierta la pregunta de qué hacer si un iterador se detiene antes que otro. Si desea continuar hasta que se agote el iterador más largo, puede usar itertools.izip_longest en lugar de itertools.izip .

 import itertools def alternate(*iters): for elt in itertools.chain.from_iterable( itertools.izip_longest(*iters)): yield elt print list(alternate(xrange(1, 7, 2), xrange(2, 10, 2))) 

Esto pondrá el rendimiento.

 [1, 2, 3, 4, 5, 6, None, 8] 

Nota None produce cuando el iterador xrange (1,7,2) genera StopIteration (no tiene más elementos).

Si solo desea omitir el iterador en lugar de ceder a None , puede hacer esto:

 Dummy=object() def alternate(*iters): for elt in itertools.chain.from_iterable( itertools.izip_longest(*iters,fillvalue=Dummy)): if elt is not Dummy: yield elt 

Si tienen la misma longitud, itertools.izip se puede aprovechar así:

 def alternate(*iters): for row in itertools.izip(*iters): for i in row: yield i 

Hay dos problemas con su bash:

  1. No envuelve cada objeto en iter() con iter() por lo que fallará con iterables como list ; y
  2. Al pass a StopIteration su generador es un bucle infinito.

Algún código simple que resuelve ambos problemas y aún es fácil de leer y entender:

 def alternate(*iters): iters = [iter(i) for i in iters] while True: for i in iters: yield next(i) >>> list(alternate(range(1, 7, 2), range(2, 8, 2))) [1, 2, 3, 4, 5, 6]