Python'da jeneratör nesnesini sıfırlama


153

Birden fazla verim ile döndürülen bir jeneratör nesnesi var. Bu jeneratörü çağırmaya hazırlık oldukça zaman alan bir işlemdir. Bu yüzden jeneratörü birkaç kez tekrar kullanmak istiyorum.

y = FunctionWithYield()
for x in y: print(x)
#here must be something to reset 'y'
for x in y: print(x)

Elbette, içeriği basit bir listeye kopyalamayı aklımdan çıkarıyorum. Jeneratörü sıfırlamanın bir yolu var mı?

Yanıtlar:


119

Başka bir seçenek, itertools.tee()jeneratörünüzün ikinci bir sürümünü oluşturmak için işlevi kullanmaktır :

y = FunctionWithYield()
y, y_backup = tee(y)
for x in y:
    print(x)
for x in y_backup:
    print(x)

Orijinal yineleme tüm öğeleri işlemeyebiliyorsa, bellek kullanımı açısından yararlı olabilir.


33
Bu durumda ne yapacağını merak ediyorsanız, aslında listedeki öğeleri önbelleğe alır. Bu nedenle y = list(y), kodunuzun geri kalanı değişmeden de kullanabilirsiniz .
ilya n.

5
tee () verileri depolamak için dahili olarak bir liste oluşturur, bu yüzden cevabımda yaptığımla aynı.
nosklo

6
Uygulamaya bakın ( docs.python.org/library/itertools.html#itertools.tee ) - bu tembel yük stratejisini kullanır, bu nedenle yalnızca talep üzerine kopyalananları listelemek için öğeler
Dewfy

11
@Dewfy: Tüm öğelerin yine de kopyalanması gerekeceğinden daha yavaş olacaktır.
nosklo

8
evet, list () bu durumda daha iyidir. tee sadece listenin tamamını tüketmiyorsanız kullanışlıdır
yerçekimi

148

Jeneratörler geri sarılamaz. Aşağıdaki seçenekleriniz vardır:

  1. Jeneratörü yeniden başlatmak için jeneratör fonksiyonunu tekrar çalıştırın:

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
  2. Jeneratör sonuçlarını tekrar tekrar yineleyebileceğiniz bellekte veya diskte bir veri yapısında saklayın:

    y = list(FunctionWithYield())
    for x in y: print(x)
    # can iterate again:
    for x in y: print(x)

Seçenek 1'in dezavantajı , değerleri tekrar hesaplamasıdır. Eğer CPU-yoğun ise, iki kez hesap yaparsınız. Öte yandan, 2'nin dezavantajı depolamadır. Tüm değerler listesi bellekte saklanır. Çok fazla değer varsa, bu pratik olmayabilir.

Yani klasik bellek ve işlem dengesi var . Değerleri saklamadan veya tekrar hesaplamadan jeneratörü geri sarmanın bir yolunu hayal edemiyorum.


İşlev çağrısının imzasını kaydetmenin bir yolu olabilir mi? FunctionWithYield, param1, param2 ...
Dewfy

3
@Dewfy: emin: def call_my_func (): return FunctionWithYield (param1, param2)
nosklo

@Dewfy "İşlev çağrısının imzasını kaydet" ile ne demek istiyorsun? Lütfen açıklar mısın? Jeneratöre geçirilen parametreleri kaydetmek mi demek istediniz?
Андрей Беньковский

2
(1) 'in bir başka dezavantajı, FunctionWithYield ()' in sadece maliyetli değil, aynı zamanda yeniden hesaplanması imkansız olabilmesidir, örneğin stdin'den okuyorsa.
Max

2
@Max'ın söylediklerini yinelemek için, işlevin çıkışı aramalar arasında değişebilirse (veya değişecekse), (1) beklenmedik ve / veya istenmeyen sonuçlar verebilir.
Sam_Butler

36
>>> def gen():
...     def init():
...         return 0
...     i = init()
...     while True:
...         val = (yield i)
...         if val=='restart':
...             i = init()
...         else:
...             i += 1

>>> g = gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
>>> g.send('restart')
0
>>> g.next()
1
>>> g.next()
2

29

Muhtemelen en basit çözüm, pahalı parçayı bir nesneye sarmak ve bunu jeneratöre aktarmaktır:

data = ExpensiveSetup()
for x in FunctionWithYield(data): pass
for x in FunctionWithYield(data): pass

Bu şekilde, pahalı hesaplamaları önbelleğe alabilirsiniz.

Tüm sonuçları RAM'de aynı anda tutabiliyorsanız list(), jeneratörün sonuçlarını düz bir listede gerçekleştirmek ve bununla çalışmak için kullanın.


23

Eski bir soruna farklı bir çözüm sunmak istiyorum

class IterableAdapter:
    def __init__(self, iterator_factory):
        self.iterator_factory = iterator_factory

    def __iter__(self):
        return self.iterator_factory()

squares = IterableAdapter(lambda: (x * x for x in range(5)))

for x in squares: print(x)
for x in squares: print(x)

Bunun yararı, benzer bir şeye kıyasla list(iterator)bunun O(1)uzay karmaşıklığı ve list(iterator)olmasıdır O(n). Dezavantajı, sadece yineleyiciye erişiminiz varsa, ancak yineleyiciyi üreten işleve sahip değilseniz, bu yöntemi kullanamazsınız. Örneğin , aşağıdakileri yapmak makul görünebilir, ancak işe yaramaz.

g = (x * x for x in range(5))

squares = IterableAdapter(lambda: g)

for x in squares: print(x)
for x in squares: print(x)

@Dewfy İlk kod parçasında, jeneratör "squares = ..." satırında. Jeneratör ifadeleri, verimi kullanan bir işlevi çağırmakla aynı şekilde davranır ve sadece bir tane kullandım çünkü bu kadar kısa bir örnek için verim ile bir işlev yazmaktan daha az ayrıntılıdır. İkinci pasajı, ben her ne zaman çağrılacak, böylece generator_factory olarak FunctionWithYield kullandım iter denir ben "y x" yazdıktan sonra hangi.
michaelsnowden

Güzel çözüm. Bu aslında durumsal bir yineleyici nesnesi yerine durumsuz bir yinelenebilir nesne yapar, böylece nesnenin kendisi yeniden kullanılabilir. Yinelenebilir bir nesneyi bir işleve geçirmek istiyorsanız ve bu işlev nesneyi birden çok kez kullanırsa özellikle kullanışlıdır.
Cosyn

5

GrzegorzOledzki'nin cevabı yeterli olmazsa, muhtemelen send()hedefinize ulaşmak için kullanabilirsiniz . Geliştirilmiş jeneratörler ve verim ifadeleri hakkında daha fazla bilgi için PEP-0342'ye bakınız .

GÜNCELLEME: Ayrıca bakınız itertools.tee(). Bu belleğin bir kısmı yukarıda belirtilen işleme dengesini içerir, ancak sadece jeneratör sonuçlarını depolamaktan biraz bellek tasarrufu sağlayabilirlist ; jeneratörü nasıl kullandığınıza bağlıdır.


5

Üreticiniz, çıktısının yalnızca geçirilen argümanlara ve adım numarasına bağlı olduğu bir anlamda safsa ve sonuçta ortaya çıkan jeneratörün yeniden başlatılmasını istiyorsanız, kullanışlı olabilecek bir tür snippet:

import copy

def generator(i):
    yield from range(i)

g = generator(10)
print(list(g))
print(list(g))

class GeneratorRestartHandler(object):
    def __init__(self, gen_func, argv, kwargv):
        self.gen_func = gen_func
        self.argv = copy.copy(argv)
        self.kwargv = copy.copy(kwargv)
        self.local_copy = iter(self)

    def __iter__(self):
        return self.gen_func(*self.argv, **self.kwargv)

    def __next__(self):
        return next(self.local_copy)

def restartable(g_func: callable) -> callable:
    def tmp(*argv, **kwargv):
        return GeneratorRestartHandler(g_func, argv, kwargv)

    return tmp

@restartable
def generator2(i):
    yield from range(i)

g = generator2(10)
print(next(g))
print(list(g))
print(list(g))
print(next(g))

çıktılar:

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

3

Tee'nin resmi belgelerinden :

Genel olarak, bir yineleyici başka bir yineleyici başlamadan önce verilerin çoğunu veya tamamını kullanırsa, tee () yerine list () kullanmak daha hızlıdır.

Bu nedenle, durumunuzda kullanmak en iyisidir list(iterable).


6
sonsuz jeneratörler ne olacak?
Dewfy

1
Hız sadece düşünülmez; list()tüm yinelenebilir hafızaya koyar
Chris_Rands

@Chris_Rands Öyleyse tee()bir yineleyici tüm değerleri tüketirse - böyle teeçalışır.
AChampion

2
@Dewfy: sonsuz jeneratörler için Aaron Digulla'nın çözümünü kullanın (Değerli verileri döndüren ExpensiveSetup işlevi.)
Jeff Learman

3

İşlemek için sarıcı işlevi kullanma StopIteration

Jeneratör tükendiğinde izleyen jeneratör üretme fonksiyonunuza basit bir sarma fonksiyonu yazabilirsiniz. Bunu, StopIterationbir jeneratörün yineleme sonuna ulaştığında attığı istisna kullanılarak yapılacaktır .

import types

def generator_wrapper(function=None, **kwargs):
    assert function is not None, "Please supply a function"
    def inner_func(function=function, **kwargs):
        generator = function(**kwargs)
        assert isinstance(generator, types.GeneratorType), "Invalid function"
        try:
            yield next(generator)
        except StopIteration:
            generator = function(**kwargs)
            yield next(generator)
    return inner_func

Yukarıda fark edebileceğiniz gibi, sarma fonksiyonumuz bir StopIterationistisna yakaladığında , jeneratör nesnesini yeniden başlatır (fonksiyon çağrısının başka bir örneğini kullanarak).

Ve sonra, jeneratör sağlama işlevinizi aşağıdaki gibi bir yerde tanımladığınızı varsayarsak, Python işlevi dekoratör sözdizimini dolaylı olarak sarmak için kullanabilirsiniz:

@generator_wrapper
def generator_generating_function(**kwargs):
    for item in ["a value", "another value"]
        yield item

2

Jeneratörünüzü döndüren bir işlev tanımlayabilirsiniz

def f():
  def FunctionWithYield(generator_args):
    code here...

  return FunctionWithYield

Şimdi istediğiniz kadar yapabilirsiniz:

for x in f()(generator_args): print(x)
for x in f()(generator_args): print(x)

1
Cevabınız için teşekkür ederim, ancak asıl soru yaratılıştan kaçınmaktı , içsel işlevi çağırmak
yaratıyı

1

Pahalı hazırlıklarla ne demek istediğinden emin değilim, ama sanırım aslında

data = ... # Expensive computation
y = FunctionWithYield(data)
for x in y: print(x)
#here must be something to reset 'y'
# this is expensive - data = ... # Expensive computation
# y = FunctionWithYield(data)
for x in y: print(x)

Bu durumda neden tekrar kullanmıyorsunuz data?


1

Yineleyicileri sıfırlama seçeneği yoktur. Yineleyici genellikle next()işlevi yinelediğinde ortaya çıkar . Tek yol, yineleyici nesnesini yinelemeden önce bir yedek almaktır. Aşağıdan kontrol edin.

0-9 arasındaki öğelerle yineleyici nesnesi oluşturma

i=iter(range(10))

Çıkacak next () işlevi ile yineleme

print(next(i))

Yineleyici nesnesini listeye dönüştürme

L=list(i)
print(L)
output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

bu yüzden 0 öğesi zaten atlanmıştır. Ayrıca yineleyiciyi listeye dönüştürdüğümüzde tüm öğeler açılır.

next(L) 

Traceback (most recent call last):
  File "<pyshell#129>", line 1, in <module>
    next(L)
StopIteration

Bu nedenle, yinelemeye başlamadan önce yineleyiciyi yedekleme listelerine dönüştürmeniz gerekir. Liste aşağıdakilerle yineleyiciye dönüştürülebiliriter(<list-object>)


1

Artık more_itertools.seekableyineleyicileri sıfırlamayı sağlayan (üçüncü taraf bir araç) kullanabilirsiniz .

Üzerinden yükle > pip install more_itertools

import more_itertools as mit


y = mit.seekable(FunctionWithYield())
for x in y:
    print(x)

y.seek(0)                                              # reset iterator
for x in y:
    print(x)

Not: yineleyici ilerlerken bellek tüketimi artar, bu nedenle büyük yinelemelere karşı dikkatli olun.


1

Bunu itertools.cycle () kullanarak bu yöntemle bir yineleyici oluşturabilir ve sonra yineleyicinin üzerinde değerlerinin üzerine döngü oluşturacak bir for döngüsü yürütebilirsiniz.

Örneğin:

def generator():
for j in cycle([i for i in range(5)]):
    yield j

gen = generator()
for i in range(20):
    print(next(gen))

0 ile 4 arasında art arda 20 sayı oluşturur.

Dokümanlardan bir not:

Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).

+1 çünkü çalışıyor, ama orada 2 sorunları görüyorum 1) büyük bellek ayak izi belgelendirme "bir kopyasını oluştur" çünkü 2) Sonsuz döngü kesinlikle istediğim gibi değil
Dewfy

0

Tamam, bir jeneratörü birden çok kez aramak istediğinizi söylüyorsunuz, ancak başlatma pahalı ... Peki ya böyle bir şey?

class InitializedFunctionWithYield(object):
    def __init__(self):
        # do expensive initialization
        self.start = 5

    def __call__(self, *args, **kwargs):
        # do cheap iteration
        for i in xrange(5):
            yield self.start + i

y = InitializedFunctionWithYield()

for x in y():
    print x

for x in y():
    print x

Alternatif olarak, yineleyici protokolünü izleyen ve bir tür 'sıfırlama' işlevi tanımlayan kendi sınıfınızı oluşturabilirsiniz.

class MyIterator(object):
    def __init__(self):
        self.reset()

    def reset(self):
        self.i = 5

    def __iter__(self):
        return self

    def next(self):
        i = self.i
        if i > 0:
            self.i -= 1
            return i
        else:
            raise StopIteration()

my_iterator = MyIterator()

for x in my_iterator:
    print x

print 'resetting...'
my_iterator.reset()

for x in my_iterator:
    print x

https://docs.python.org/2/library/stdtypes.html#iterator-types http://anandology.com/python-practice-book/iterators.html


Sadece sarýcýya sorun delege ediyorsun. Pahalı başlatmanın jeneratör oluşturduğunu varsayın. Benim sorum senin içinde sıfırlama hakkındaydı__call__
Dewfy

Yorumunuza yanıt olarak ikinci bir örnek eklendi. Bu aslında bir sıfırlama yöntemi ile özel bir jeneratör.
tvt173

0

Cevabım biraz farklı sorunu çözüyor: Jeneratörün başlatılması pahalıysa ve üretilen her bir nesnenin yaratılması pahalıysa. Ancak jeneratörü birden fazla fonksiyonda birden fazla tüketmeliyiz. Üreticiyi ve oluşturulan her nesneyi tam olarak çağırmak için, iş parçacıklarını kullanabilir ve tüketen yöntemlerin her birini farklı iş parçacığında çalıştırabiliriz. GIL nedeniyle gerçek paralellik elde edemeyebiliriz, ancak hedefimize ulaşacağız.

Bu yaklaşım aşağıdaki durumda iyi bir iş çıkardı: derin öğrenme modeli çok fazla görüntü işler. Sonuç, görüntüdeki birçok nesne için çok sayıda maskedir. Her maske bellek tüketir. Farklı istatistikler ve metrikler üreten yaklaşık 10 yöntemimiz var, ancak tüm görüntüleri bir kerede alıyorlar. Tüm görüntüler belleğe sığamaz. Moethods, yineleyiciyi kabul etmek için kolayca yeniden yazılabilir.

class GeneratorSplitter:
'''
Split a generator object into multiple generators which will be sincronised. Each call to each of the sub generators will cause only one call in the input generator. This way multiple methods on threads can iterate the input generator , and the generator will cycled only once.
'''

def __init__(self, gen):
    self.gen = gen
    self.consumers: List[GeneratorSplitter.InnerGen] = []
    self.thread: threading.Thread = None
    self.value = None
    self.finished = False
    self.exception = None

def GetConsumer(self):
    # Returns a generator object. 
    cons = self.InnerGen(self)
    self.consumers.append(cons)
    return cons

def _Work(self):
    try:
        for d in self.gen:
            for cons in self.consumers:
                cons.consumed.wait()
                cons.consumed.clear()

            self.value = d

            for cons in self.consumers:
                cons.readyToRead.set()

        for cons in self.consumers:
            cons.consumed.wait()

        self.finished = True

        for cons in self.consumers:
            cons.readyToRead.set()
    except Exception as ex:
        self.exception = ex
        for cons in self.consumers:
            cons.readyToRead.set()

def Start(self):
    self.thread = threading.Thread(target=self._Work)
    self.thread.start()

class InnerGen:
    def __init__(self, parent: "GeneratorSplitter"):
        self.parent: "GeneratorSplitter" = parent
        self.readyToRead: threading.Event = threading.Event()
        self.consumed: threading.Event = threading.Event()
        self.consumed.set()

    def __iter__(self):
        return self

    def __next__(self):
        self.readyToRead.wait()
        self.readyToRead.clear()
        if self.parent.finished:
            raise StopIteration()
        if self.parent.exception:
            raise self.parent.exception
        val = self.parent.value
        self.consumed.set()
        return val

ussage:

genSplitter = GeneratorSplitter(expensiveGenerator)

metrics={}
executor = ThreadPoolExecutor(max_workers=3)
f1 = executor.submit(mean,genSplitter.GetConsumer())
f2 = executor.submit(max,genSplitter.GetConsumer())
f3 = executor.submit(someFancyMetric,genSplitter.GetConsumer())
genSplitter.Start()

metrics.update(f1.result())
metrics.update(f2.result())
metrics.update(f3.result())

Sen sadece yeniden icat itertools.isliceya da async için aiostream.stream.take, ve bu yazı asyn / bekliyor şekilde yapmanıza izin verir stackoverflow.com/a/42379188/149818
Dewfy

-3

Kod nesnesi tarafından yapılabilir. İşte örnek.

code_str="y=(a for a in [1,2,3,4])"
code1=compile(code_str,'<string>','single')
exec(code1)
for i in y: print i

1 2 3 4

for i in y: print i


exec(code1)
for i in y: print i

1 2 3 4


4
aslında, başlatma kodunun iki kez yürütülmesini önlemek için jeneratörü sıfırlamak gerekiyordu. Yaklaşımınız (1) başlatmayı yine de iki kez yürütür, (2) bu exectür basit durumlar için biraz tavsiye edilmez.
Dewfy
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.