Mereset objek generator dengan Python

153

Saya memiliki objek generator yang dikembalikan oleh banyak hasil. Persiapan untuk memanggil generator ini agak memakan waktu operasi. Itu sebabnya saya ingin menggunakan kembali generator beberapa kali.

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

Tentu saja, saya mempertimbangkan menyalin konten ke dalam daftar sederhana. Apakah ada cara untuk mereset generator saya?

Dewfy
sumber

Jawaban:

119

Pilihan lain adalah menggunakan itertools.tee()fungsi untuk membuat versi kedua generator Anda:

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

Ini bisa menguntungkan dari sudut pandang penggunaan memori jika iterasi asli mungkin tidak memproses semua item.

Semut Aasma
sumber
33
Jika Anda bertanya-tanya tentang apa yang akan dilakukan dalam kasus ini, pada dasarnya itu adalah elemen caching dalam daftar. Jadi sebaiknya Anda gunakan y = list(y)dengan sisa kode Anda tidak berubah.
ilya n.
5
tee () akan membuat daftar secara internal untuk menyimpan data, jadi itu sama dengan yang saya lakukan dalam jawaban saya.
nosklo
6
Lihatlah implmentasi ( docs.python.org/library/itertools.html#itertools.tee ) - ini menggunakan strategi pemuatan yang malas, jadi item-item yang ada di daftar hanya disalin berdasarkan permintaan
Dewfy
11
@Dewfy: Yang akan lebih lambat karena semua item harus disalin.
nosklo
8
ya, list () lebih baik dalam hal ini. tee hanya berguna jika Anda tidak mengkonsumsi seluruh daftar
gravitasi
148

Generator tidak dapat diputar ulang. Anda memiliki opsi berikut:

  1. Jalankan kembali fungsi generator, mulai ulang generasi:

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
  2. Menyimpan hasil generator dalam struktur data pada memori atau disk yang dapat Anda ulangi lagi:

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

Kelemahan dari opsi 1 adalah bahwa ia menghitung nilai lagi. Jika CPU-intensif Anda akhirnya menghitung dua kali. Di sisi lain, kekurangan 2 adalah penyimpanan. Seluruh daftar nilai akan disimpan di memori. Jika ada terlalu banyak nilai, itu bisa tidak taktis.

Jadi, Anda memiliki memori klasik vs pemrosesan pengorbanan . Saya tidak bisa membayangkan cara memutar generator tanpa menyimpan nilai-nilai atau menghitungnya lagi.

nosklo
sumber
Mungkin ada cara untuk menyimpan tanda tangan panggilan fungsi? FunctionWithYield, param1, param2 ...
Dewfy
3
@Dewfy: sure: def call_my_func (): return FunctionWithYield (param1, param2)
nosklo
@Dewfy Apa yang Anda maksud dengan "simpan tanda tangan panggilan fungsi"? Bisakah Anda jelaskan? Apakah maksud Anda menyimpan parameter yang diteruskan ke generator?
Андрей Беньковский
2
Kelemahan lain dari (1) juga bahwa FunctionWithYield () dapat tidak hanya mahal, tetapi tidak mungkin untuk menghitung ulang, misalnya jika membaca dari stdin.
Maks
2
Untuk menggemakan apa yang dikatakan @Max, jika output fungsi mungkin (atau akan) berubah di antara panggilan, (1) dapat memberikan hasil yang tidak terduga dan / atau tidak diinginkan.
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
aaab
sumber
29

Mungkin solusi paling sederhana adalah dengan membungkus bagian yang mahal dalam suatu objek dan meneruskannya ke generator:

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

Dengan cara ini, Anda dapat men-cache perhitungan yang mahal.

Jika Anda dapat menyimpan semua hasil dalam RAM secara bersamaan, gunakan list()untuk mematerialisasikan hasil generator dalam daftar sederhana dan bekerja dengannya.

Aaron Digulla
sumber
23

Saya ingin menawarkan solusi berbeda untuk masalah lama

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)

Manfaat ini jika dibandingkan dengan sesuatu seperti list(iterator)ini adalah O(1)kompleksitas ruang dan apa list(iterator)adanya O(n). Kerugiannya adalah, jika Anda hanya memiliki akses ke iterator, tetapi bukan fungsi yang menghasilkan iterator, maka Anda tidak dapat menggunakan metode ini. Misalnya, mungkin masuk akal untuk melakukan hal berikut, tetapi tidak akan berhasil.

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

squares = IterableAdapter(lambda: g)

for x in squares: print(x)
for x in squares: print(x)
michaelsnowden
sumber
@Dewfy Dalam cuplikan pertama, generator ada di baris "kotak = ...". Ekspresi generator berperilaku sama seperti memanggil fungsi yang menggunakan hasil, dan saya hanya menggunakan satu karena itu kurang verbose daripada menulis fungsi dengan hasil untuk contoh singkat. Dalam cuplikan kedua, saya telah menggunakan FunctionWithYield sebagai generator_factory, sehingga akan dipanggil setiap kali iter dipanggil, yang setiap kali saya menulis "for x in y".
michaelsnowden
Solusi yang bagus Ini sebenarnya membuat objek iterable stateless bukan objek iterator stateful, jadi objek itu sendiri dapat digunakan kembali. Terutama berguna jika Anda ingin meneruskan objek yang dapat diubah ke fungsi dan fungsi itu akan menggunakan objek beberapa kali.
Cosyn
5

Jika jawaban GrzegorzOledzki tidak cukup, Anda mungkin dapat menggunakan send()untuk mencapai tujuan Anda. Lihat PEP-0342 untuk detail lebih lanjut tentang generator yang ditingkatkan dan ekspresi hasil.

UPDATE: Lihat juga itertools.tee(). Ini melibatkan beberapa memori yang dibandingkan dengan pemrosesan tradeoff yang disebutkan di atas, tetapi mungkin menghemat beberapa memori lebih dari sekadar menyimpan hasil generator dalam list; itu tergantung pada bagaimana Anda menggunakan generator.

Hank Gay
sumber
5

Jika generator Anda murni dalam arti bahwa outputnya hanya bergantung pada argumen yang diteruskan dan nomor langkah, dan Anda ingin generator yang dihasilkan dapat di-restart, berikut ini cuplikan potongan yang mungkin berguna:

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

output:

[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
Ben Usman
sumber
3

Dari dokumentasi resmi tee :

Secara umum, jika satu iterator menggunakan sebagian besar atau semua data sebelum iterator lain dimulai, lebih cepat menggunakan list () daripada tee ().

Jadi yang terbaik adalah menggunakan list(iterable)dalam kasus Anda.

Shubham Chaudhary
sumber
6
bagaimana dengan generator yang tak terbatas?
Dewfy
1
Kecepatan bukan satu-satunya pertimbangan; list()menempatkan seluruh iterable ke dalam memori
Chris_Rands
@ Chris_Rands Begitu juga tee()jika satu iterator mengkonsumsi semua nilai - begitulah cara teekerjanya.
AChampion
2
@Dewfy: untuk generator tanpa batas, gunakan solusi Aaron Digulla (fungsi ExpensiveSetup mengembalikan data berharga).
Jeff Learman
3

Menggunakan fungsi pembungkus untuk menangani StopIteration

Anda bisa menulis fungsi pembungkus sederhana untuk fungsi generator yang melacak ketika generator habis. Ini akan melakukannya dengan menggunakan StopIterationpengecualian yang dilempar generator saat mencapai akhir iterasi.

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

Seperti yang Anda lihat di atas, ketika fungsi wrapper kami menangkap StopIterationpengecualian, ia hanya menginisialisasi ulang objek generator (menggunakan instance lain dari panggilan fungsi).

Dan kemudian, dengan asumsi Anda mendefinisikan fungsi penyediaan generator di suatu tempat seperti di bawah ini, Anda bisa menggunakan sintaks dekorator fungsi Python untuk membungkusnya secara implisit:

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

Anda dapat mendefinisikan fungsi yang mengembalikan generator Anda

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

  return FunctionWithYield

Sekarang Anda bisa melakukan sebanyak yang Anda suka:

for x in f()(generator_args): print(x)
for x in f()(generator_args): print(x)
SMeznaric
sumber
1
Terima kasih atas jawabannya, tetapi poin utama pertanyaannya adalah menghindari penciptaan , menjalankan fungsi batin hanya menyembunyikan penciptaan - Anda membuatnya dua kali
Dewfy
1

Saya tidak yakin apa yang Anda maksud dengan persiapan mahal, tapi saya kira Anda benar-benar memilikinya

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)

Jika itu masalahnya, mengapa tidak digunakan kembali data?

ilya n.
sumber
1

Tidak ada opsi untuk mengatur ulang iterator. Iterator biasanya muncul ketika iterate melalui next()fungsi. Satu-satunya cara adalah mengambil cadangan sebelum beralih pada objek iterator. Periksa di bawah.

Membuat objek iterator dengan item 0 hingga 9

i=iter(range(10))

Iterasi melalui fungsi next () yang akan muncul

print(next(i))

Konversi objek iterator ke daftar

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

jadi item 0 sudah muncul. Juga semua item muncul saat kami mengonversi iterator ke daftar.

next(L) 

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

Jadi, Anda perlu mengonversi iterator ke daftar untuk cadangan sebelum mulai mengulangi. Daftar dapat dikonversi ke iterator denganiter(<list-object>)

Kemenangan Amalraj
sumber
1

Anda sekarang dapat menggunakan more_itertools.seekable(alat pihak ketiga) yang memungkinkan pengaturan ulang iterator.

Instal via > 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)

Catatan: konsumsi memori tumbuh saat memajukan iterator, jadi berhati-hatilah dengan iterables besar.

pylang
sumber
1

Anda bisa melakukannya dengan menggunakan itertools.cycle () Anda bisa membuat iterator dengan metode ini dan kemudian menjalankan for for over the iterator yang akan mengulang nilainya.

Sebagai contoh:

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

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

akan menghasilkan 20 angka, 0 hingga 4 berulang kali.

Catatan dari dokumen:

Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).
SajanGohil
sumber
+1 karena berfungsi, tapi saya melihat 2 masalah di sana 1) jejak memori besar karena dokumentasi menyatakan "buat salinan" 2) Infinite loop jelas bukan yang saya inginkan
Dewfy
0

Ok, Anda bilang ingin memanggil generator beberapa kali, tetapi inisialisasi mahal ... Bagaimana dengan yang seperti ini?

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

Atau, Anda bisa membuat kelas Anda sendiri yang mengikuti protokol iterator dan mendefinisikan semacam fungsi 'reset'.

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

tvt173
sumber
Anda hanya mendelegasikan masalah ke pembungkus. Asumsikan inisialisasi yang mahal menghasilkan generator. Pertanyaan saya adalah tentang cara mengatur ulang di dalam Anda__call__
Dewfy
Menambahkan contoh kedua sebagai tanggapan atas komentar Anda. Ini pada dasarnya generator khusus dengan metode reset.
tvt173
0

Jawaban saya memecahkan masalah yang sedikit berbeda: Jika generator mahal untuk diinisialisasi dan setiap objek yang dihasilkan mahal untuk dihasilkan. Tetapi kita perlu mengkonsumsi generator beberapa kali dalam berbagai fungsi. Untuk memanggil generator dan setiap objek yang dihasilkan tepat sekali kita dapat menggunakan utas dan Jalankan masing-masing metode pengkonsumsi dalam utas yang berbeda. Kami mungkin tidak mencapai paralelisme sejati karena GIL, tetapi kami akan mencapai tujuan kami.

Pendekatan ini melakukan pekerjaan yang baik dalam kasus berikut: model pembelajaran yang mendalam memproses banyak gambar. Hasilnya banyak topeng untuk banyak objek pada gambar. Setiap topeng mengkonsumsi memori. Kami memiliki sekitar 10 metode yang membuat statistik dan metrik berbeda, tetapi mereka mengambil semua gambar sekaligus. Semua gambar tidak dapat ditampung dalam memori. Metode-metode tersebut dapat dengan mudah ditulis ulang untuk menerima iterator.

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

Penggunaan:

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())
Asen
sumber
Anda baru saja menemukan kembali itertools.isliceatau untuk async aiostream.stream.take, dan postingan ini memungkinkan Anda melakukannya dalam asyn / menunggu cara stackoverflow.com/a/42379188/149818
Dewfy
-3

Itu bisa dilakukan oleh objek kode. Inilah contohnya.

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

OlegOS
sumber
4
baik, sebenarnya mengatur ulang generator diperlukan untuk menghindari eksekusi kode inisialisasi dua kali. Pendekatan Anda (1) melakukan inisialisasi dua kali, (2) melibatkan execsedikit yang tidak direkomendasikan untuk kasus sederhana tersebut.
Dewfy