Quiero analizar 2 generadores de (potencialmente) longitud diferente con zip
:
for el1, el2 in zip(gen1, gen2): print(el1, el2)
Sin embargo, si gen2
tiene menos elementos, se "consume" un elemento adicional de gen1
.
Por ejemplo,
def my_gen(n:int): for i in range(n): yield i gen1 = my_gen(10) gen2 = my_gen(8) list(zip(gen1, gen2)) # Last tuple is (7, 7) print(next(gen1)) # printed value is "9" => 8 is missing gen1 = my_gen(8) gen2 = my_gen(10) list(zip(gen1, gen2)) # Last tuple is (7, 7) print(next(gen2)) # printed value is "8" => OK
Aparentemente, falta un valor ( 8
en mi ejemplo anterior) porque se lee gen1
(generando así el valor 8
) antes de darse cuenta de que gen2
no tiene más elementos. Pero este valor desaparece en el universo. Cuando gen2
es "más largo", no hay tal "problema".
PREGUNTA : ¿Hay alguna forma de recuperar este valor faltante (es decir, 8
en mi ejemplo anterior)? ... idealmente con un número variable de argumentos (como lo hace zip
).
NOTA : actualmente lo he implementado de otra manera usando itertools.zip_longest
pero realmente me pregunto cómo obtener este valor faltante usando zip
o equivalente.
NOTA 2 : he creado algunas pruebas de las diferentes implementaciones en este REPL en caso de que desee enviar y probar una nueva implementación :) https://repl.it/@jfthuong/MadPhysicistChester
Este es el equivalente de implementación zip
dado en los documentos
def zip(*iterables): # zip('ABCD', 'xy') --> Ax By sentinel = object() iterators = [iter(it) for it in iterables] while iterators: result = [] for it in iterators: elem = next(it, sentinel) if elem is sentinel: return result.append(elem) yield tuple(result)
En su primer ejemplo gen1 = my_gen(10)
y gen2 = my_gen(8)
. Después de que ambos generadores se consuman hasta la séptima iteración. Ahora, en la octava iteración, gen1
llama a elem = next(it, sentinel)
que devuelve 8 pero cuando gen2
llama a elem = next(it, sentinel)
devuelve sentinel
(porque en este gen2
está agotado) y if elem is sentinel
se satisface y se ejecuta la función regresa y se detiene. Ahora next(gen1)
devuelve 9.
En su segundo ejemplo gen1 = gen(8)
y gen2 = gen(10)
. Después de que ambos generadores se consuman hasta la séptima iteración. Ahora, en la octava iteración, gen1
llama a elem = next(it, sentinel)
que devuelve sentinel
(porque en este punto gen1
está agotado) y if elem is sentinel
está satisfecho, la función ejecuta return y se detiene. Ahora next(gen2)
devuelve 8.
Inspirado en la respuesta de Mad Physicist , podrías usar este envoltorio Gen
para contrarrestarlo:
Editar : Para manejar los casos señalados por Jean-Francois T.
Una vez que se consume un valor del iterador, desaparece para siempre del iterador y no hay un método de mutación en el lugar para que los iteradores lo vuelvan a agregar al iterador. Una solución consiste en almacenar el último valor consumido.
class Gen: def __init__(self,iterable): self.d = iter(iterable) self.sentinel = object() self.prev = self.sentinel def __iter__(self): return self @property def last_val_consumed(self): if self.prev is None: raise StopIteration if self.prev == self.sentinel: raise ValueError('Nothing has been consumed') return self.prev def __next__(self): self.prev = next(self.d,None) if self.prev is None: raise StopIteration return self.prev
Ejemplos:
# When `gen1` is larger than `gen2` gen1 = Gen(range(10)) gen2 = Gen(range(8)) list(zip(gen1,gen2)) # [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7)] gen1.last_val_consumed # 8 #as it was the last values consumed next(gen1) # 9 gen1.last_val_consumed # 9 # 2. When `gen1` or `gen2` is empty gen1 = Gen(range(0)) gen2 = Gen(range(5)) list(zip(gen1,gen2)) gen1.last_val_consumed # StopIteration error is raised gen2.last_val_consumed # ValueError is raised saying `ValueError: Nothing has been consumed`
Una forma sería implementar un generador que le permita almacenar en caché el último valor:
class cache_last(collections.abc.Iterator): """ Wraps an iterable in an iterator that can retrieve the last value. .. attribute:: obj A reference to the wrapped iterable. Provided for convenience of one-line initializations. """ def __init__(self, iterable): self.obj = iterable self._iter = iter(iterable) self._sentinel = object() @property def last(self): """ The last object yielded by the wrapped iterator. Uninitialized iterators raise a `ValueError`. Exhausted iterators raise a `StopIteration`. """ if self.exhausted: raise StopIteration return self._last @property def exhausted(self): """ `True` if there are no more elements in the iterator. Violates EAFP, but convenient way to check if `last` is valid. Raise a `ValueError` if the iterator is not yet started. """ if not hasattr(self, '_last'): raise ValueError('Not started!') return self._last is self._sentinel def __next__(self): """ Retrieve, record, and return the next value of the iteration. """ try: self._last = next(self._iter) except StopIteration: self._last = self._sentinel raise # An alternative that has fewer lines of code, but checks # for the return value one extra time, and loses the underlying # StopIteration: #self._last = next(self._iter, self._sentinel) #if self._last is self._sentinel: # raise StopIteration return self._last def __iter__(self): """ This object is already an iterator. """ return self
Para usar esto, envuelva las entradas en zip
:
gen1 = cache_last(range(10)) gen2 = iter(range(8)) list(zip(gen1, gen2)) print(gen1.last) print(next(gen1))
Es importante hacer que gen2
sea un iterador en lugar de un iterable, para que pueda saber cuál se agotó. Si gen2
está agotado, no necesita verificar gen1.last
.
Otro enfoque sería anular zip para aceptar una secuencia mutable de iterables en lugar de iterables separados. Eso le permitiría reemplazar iterables con una versión encadenada que incluye su elemento "mirado":
def myzip(iterables): iterators = [iter(it) for it in iterables] while True: items = [] for it in iterators: try: items.append(next(it)) except StopIteration: for i, peeked in enumerate(items): iterables[i] = itertools.chain([peeked], iterators[i]) return else: yield tuple(items) gens = [range(10), range(8)] list(myzip(gens)) print(next(gens[0]))
Este enfoque es problemático por muchas razones. No solo perderá el iterable original, sino que perderá cualquiera de las propiedades útiles que el objeto original pudo haber tenido al reemplazarlo con un objeto de chain
.
Puedo ver que ya encontraste esta respuesta y se mencionó en los comentarios, pero pensé que haría una respuesta a partir de ella. Desea usar itertools.zip_longest()
, que reemplazará los valores vacíos del generador más corto con None
:
import itertools def my_gen(n:int): for i in range(n): yield i gen1 = my_gen(10) gen2 = my_gen(8) for i, j in itertools.zip_longest(gen1, gen2): print(i, j)
Huellas dactilares:
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 None 9 None
También puede proporcionar un argumento de valor de relleno cuando llame a fillvalue
para reemplazar None
con un valor predeterminado, pero básicamente para su solución una vez que zip_longest
None
(ya sea i
o j
) en el ciclo for, la otra variable tendrá su 8
.
Inspirándonos en la elucidación de zip
de @GrandPhuba, creemos una variante "segura" (unidad probada aquí ):
def safe_zip(*args): """ Safe zip that restores last consumed element in eachgenerator if not able to consume an element in all of them Returns: * generators in tuple * generator for zipped generators """ continue_ = True n = len(args) result = (_ for _ in []) while continue_: addend = [] for i, gen in enumerate(args): try: value = next(gen) addend.append(value) except StopIteration: genlist = list(args) args = tuple([chain([v], g) for v, g in zip(addend, genlist[:i])]+genlist[i:]) continue_ = False break if len(addend)==n: result = chain(result, [tuple(addend)]) return args, result
Aquí hay una prueba básica:
g1, g2 = (i for i in range(10)), (i for i in range(4)) # Create (g1, g2), g3 first, then loop over g3 as one would with zip (g1, g2), g3 = safe_zip(g1, g2) for a, b in g3: print(a, b)#(0, 0) to (3, 3) for x in g1: print(x)#4 to 9
No creo que pueda recuperar el valor eliminado con el bucle for básico, porque el iterador agotado, tomado de zip(..., ...).__iter__
se elimina una vez agotado y no puede acceder a él.
Debes mutar tu zip, luego puedes obtener la posición del elemento caído con algún código hacky)
z = zip(range(10), range(8)) for _ in iter(z.__next__, None): ... _, (one, other) = z.__reduce__() _, (i_one,), p_one = one.__reduce__() # p_one == current pos, 1 based import itertools val = next(itertools.islice(iter(i_one), p_one - 1, p_one))
Si desea reutilizar el código, la solución más fácil es:
from more_itertools import peekable a = peekable(a) b = peekable(b) while True: try: a.peek() b.peek() except StopIteration: break x = next(a) y = next(b) print(x, y) print(list(a), list(b)) # Misses nothing.
Puede probar este código usando su configuración:
def my_gen(n: int): yield from range(n) a = my_gen(10) b = my_gen(8)
Imprimirá:
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 [8, 9] []
podrías usar itertools.tee e itertools.islice :
from itertools import islice, tee def zipped(gen1, gen2, pred=list): g11, g12 = tee(gen1) z = pred(zip(g11, gen2)) return (islice(g12, len(z), None), gen2), z gen1 = iter(range(10)) gen2 = iter(range(5)) (gen1, gen2), output = zipped(gen1, gen2) print(output) print(next(gen1)) # [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)] # 5
Nada más sacarlo de la caja, zip() está programado para desechar el artículo que no coincide. Por lo tanto, necesita una forma de recordar los valores antes de que se consuman.
El itertool llamado tee() fue diseñado para este propósito. Puede usarlo para crear una "sombra" del primer iterador de entrada. Si el segundo iterador termina, puede obtener el valor del primer iterador del iterador oculto.
Esta es una forma de hacerlo que utiliza las herramientas existentes, que se ejecuta a velocidad C y que es eficiente en memoria:
>>> from itertools import tee >>> from operator import itemgetter >>> iterable1, iterable2 = 'abcde', 'xyz' >>> it1, shadow1 = tee(iterable1) >>> it2 = iter(iterable2) >>> combined = map(itemgetter(0, 1), zip(it1, it2, shadow1)) >>> list(combined) [('a', 'x'), ('b', 'y'), ('c', 'z')] >>> next(shadow1) 'd'