Sıkıştırılmış Python jeneratörleri, ikincisi daha kısa: sessizce tüketilen elemanın nasıl alınacağı


50

Aşağıdakilerle (potansiyel olarak) farklı uzunlukta 2 jeneratör ayrıştırmak istiyorum zip:

for el1, el2 in zip(gen1, gen2):
    print(el1, el2)

Ancak, gen2daha az eleman varsa , fazladan bir eleman gen1"tüketilir".

Örneğin,

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

Görünüşe göre, bir değer eksik ( 8önceki örneğimde) çünkü fark edilmeden gen1okunan (böylece değeri üreten 8) gen2başka unsur yok. Ancak bu değer evrende kaybolur. Ne zaman gen2"uzun" olduğunu, böyle bir "sorun" var.

SORU : Bu eksik değeri almanın bir yolu var mı (yani 8önceki örneğimde)? ... ideal olarak değişken sayıda argümanla (olduğu gibi zip).

NOT : Şu anda kullanarak başka bir şekilde uyguladım itertools.zip_longestama gerçekten bu eksik değeri zipveya eşdeğerini kullanarak nasıl elde edeceğini merak ediyorum .

NOT 2 : Yeni bir uygulamayı göndermek ve denemek istemeniz durumunda, bu REPL'deki farklı uygulamaların bazı testlerini oluşturdum :) https://repl.it/@jfthuong/MadPhysicistChester


19
Dokümanlar, "zip () işlevinin yalnızca uzun, yinelenebilir değerlerden gelen eşleşmeyen değerlerle eşleşmeyen değerlerde yalnızca eşit olmayan uzunluk girişleriyle kullanılması gerektiğini unutmayın. Bu değerler önemliyse, bunun yerine itertools.zip_longest () kullanın."
Carcigenicate

2
@ Ch3steR. Ancak sorunun "neden" ile ilgisi yoktur. Kelimenin tam anlamıyla "Bu eksik değeri almanın bir yolu var mı?" Görünüşe göre benimki dışındaki tüm cevaplar bu bölümü okumayı uygun bir şekilde unuttu.
Mad Physicist

@MadPhysicist Gerçekten garip. Soruyu bu açıdan daha açık olması için yeniden ifade ettim.
Jean-Francois T.

1
Temel sorun, bir jeneratöre bakmanın veya geri çekmenin bir yolu olmamasıdır. Bir kez zip()okuduktan 8sonra gen1gitti.
Barmar

1
@ Barmar kesinlikle, hepimiz buna karar verdik. Soru, onu kullanabilmek için bir yerde nasıl saklanacağıydı.
Jean-Francois T.

Yanıtlar:


28

Bunun bir yolu, son değeri önbelleğe almanızı sağlayan bir jeneratör uygulamak olacaktır:

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

Bunu kullanmak için girişleri şuraya sarın zip:

gen1 = cache_last(range(10))
gen2 = iter(range(8))
list(zip(gen1, gen2))
print(gen1.last)
print(next(gen1)) 

gen2Yinelenebilir bir yineleyici yapmak önemlidir , böylece hangisinin tükendiğini öğrenebilirsiniz. Eğer gen2tükenmiş, denetlemek gerekmez gen1.last.

Başka bir yaklaşım, ayrı tekrarlanabilirler yerine değiştirilebilir bir tekrarlanabilir dizisi kabul etmek için zip'i geçersiz kılmak olacaktır. Bu, yinelenebilirleri "gözetimli" öğenizi içeren zincirleme bir sürümle değiştirmenize olanak tanır:

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]))

Bu yaklaşım birçok nedenden dolayı sorunludur. Sadece orijinal yinelemeyi kaybetmekle kalmaz, aynı zamanda orijinal nesnenin sahip olduğu yararlı özelliklerden herhangi birini bir chainnesneyle değiştirerek kaybeder .


@MadPhysicist. Cevabınızı cache_lastve nextdavranışı değiştirmemesi gerçeğini seviyorum ... o kadar kötü simetrik değil (geçiş yapmak gen1ve gen2zip yapmak farklı sonuçlara yol açacaktır). Cheers
Jean-Francois T.

1
@ Jean-Francois. Yorgun lastolduktan sonra çağrılara düzgün yanıt vermek için yineleyiciyi güncelledim . Bu, son değere ihtiyacınız olup olmadığını anlamanıza yardımcı olacaktır. Ayrıca daha üretim-y yapar.
Mad Physicist

@MadPhysicist Ben kodunu ve çıktısını ran print(gen1.last) print(next(gen1)) DİRNone and 9
Ch3steR

@MadPhysicist bazı öğretileri ve her şeyi. Güzel;) Zamanım olduğunda daha sonra kontrol edeceğim. Harcanan zaman için teşekkürler
Jean-Francois T.

@ Ch3steR. Yakaladığınız için teşekkürler. Çok heyecanlandım ve dönüş ifadesini sildim last.
Mad Physicist

17

Bu, dokümanlardazip verilen uygulama eşdeğeri

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)

1. örneğinizde gen1 = my_gen(10)ve gen2 = my_gen(8). Her iki jeneratör de 7. iterasyona kadar tüketilir. Şimdi 8 yineleme içinde gen1aramaları elem = next(it, sentinel)8 dönmek ama ne zaman hangi gen2aramalar elem = next(it, sentinel)döndürür sentinel(şuna çünkü gen2tükenmiş) ve if elem is sentinelmemnun olduğunu ve işlev yürütür dönmek ve durur. Şimdi next(gen1)9 döndürür.

2. örneğinizde gen1 = gen(8)ve gen2 = gen(10). Her iki jeneratör de 7. iterasyona kadar tüketilir. Şimdi 8 yineleme içinde gen1aramaların elem = next(it, sentinel)hangi döner sentinel(çünkü bu noktada gen1tükenmiş) ve if elem is sentinelmemnun olduğunu ve işlev yürütür dönmek ve durur. Şimdi next(gen2)8 döndürür.

Mad Physicist'in cevabından esinlenerek , bu Gensargıyı karşı koymak için kullanabilirsiniz:

Edit : Jean-Francois T. tarafından işaret edilen davaları ele almak için

Yineleyiciden bir değer tüketildiğinde, yineleyiciden sonsuza kadar gider ve yineleyicilerin yineleyiciye geri eklemesi için yerinde mutasyon yöntemi yoktur. Bir çözüm, son tüketilen değeri saklamaktır.

class Gen:
    def __init__(self,iterable):
        self.d = iter(iterable)
        self.sentinal = object()
        self.prev = self.sentinal
    def __iter__(self):
        return self
    @property
    def last_val_consumed(self):
        if self.prev is None:
            raise StopIteration
        if self.prev == self.sentinal:
            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

Örnekler:

# 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`

@ Ch3steR bu sorun için harcanan zaman için teşekkür ederiz. MadPhysicist çözümü üzerinde yaptığınız değişikliğin bazı sınırlamaları vardır: # 1. Eğer gen1 = cache_last(range(0))ve gen2 = cache_last(range(2))sonra yaptıktan sonra list(zip(gen1, gen2), bir çağrı next(gen2)bir AttributeError: 'cache_last' object has no attribute 'prev'. # 2. Gen1 gen2'den uzunsa, tüm öğeleri tükettikten sonra, next(gen2)yerine son değeri döndürmeye devam eder StopIteration. MadPhysicist cevabını ve THE cevabını işaretleyeceğim. Teşekkürler!
Jean-Francois T.10

@ Jean-FrancoisT. Evet kabul edildi. Cevabını cevap olarak işaretlemelisin. Bunun sınırlamaları vardır. Tüm vakalara karşı koymak için bu cevabı geliştirmeye çalışacağım. ;)
Ch3steR

@ Ch3steR İsterseniz sallayarak size yardımcı olabilirim. Yazılım Doğrulama alanında profesyonelim :)
Jean-Francois T.

@ Jean-FrancoisT. İsterdim. Çok şey ifade eder. Ben 3. sınıf lisans öğrencisiyim.
Ch3steR

2
İyi iş, burada yazdığım tüm testleri geçiyor: repl.it/@jfthuong/MadPhysicistChester Onları çevrimiçi çalıştırabilirsiniz, oldukça uygun :)
Jean-Francois T.

6

Bu cevabı zaten bulduğunuzu görebiliyorum ve yorumlarda gündeme geldi ama anladım ki cevap vereceğim. Daha itertools.zip_longest()kısa jeneratörün boş değerlerinin yerini alacak olan kullanmak istiyorsunuz 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)

Baskılar:

0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 None
9 None

Ayrıca , varsayılan bir değerle değiştirilmek üzere fillvalueçağrıldığında bir bağımsız değişken de sağlayabilirsiniz , ancak temelde for döngüsünde bir (ya da ) tuşuna bastığınızda çözümünüz için diğer değişken sizin .zip_longestNoneNoneij8


Teşekkürler. Gerçekten çoktan geldim zip_longestve aslında benim sorum vardı. :)
Jean-Francois T.

6

@ GrandPhuba'nın açıklanmasından esinlenerek zip, "güvenli" bir varyant oluşturalım ( burada birim test edilir ):

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

İşte temel bir test:

    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

4

itertools.tee ve itertools.islice kullanabilirsiniz :

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

3

Kodu yeniden kullanmak istiyorsanız, en kolay çözüm:

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.

Bu kodu kurulumunuzu kullanarak test edebilirsiniz:

def my_gen(n: int):
    yield from range(n)

a = my_gen(10)
b = my_gen(8)

Yazdırılacak:

0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
[8, 9] []

2

Ben bitkin yineleyici, bir zip(..., ...).__iter__kez bitkin düştü alınan alınan ve döngü olamaz çünkü döngü için temel ile düştü değeri alabilirsiniz sanmıyorum .

Zip'inizi değiştirmelisiniz, daha sonra bazı hileli kodlarla bırakılan öğenin konumunu alabilirsiniz)

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))
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.