Memahami generator dengan Python

218

Saya sedang membaca buku masak Python saat ini dan saya sedang melihat generator. Saya merasa sulit untuk mendapatkan kepalaku.

Karena saya berasal dari latar belakang Java, apakah ada yang setara dengan Java? Buku itu berbicara tentang 'Produser / Konsumen', namun ketika saya mendengar bahwa saya berpikir tentang threading.

Apa itu generator dan mengapa Anda menggunakannya? Tanpa mengutip buku apa pun, jelas (kecuali Anda dapat menemukan jawaban yang layak dan sederhana langsung dari buku). Mungkin dengan contoh, jika Anda merasa murah hati!

Federer
sumber

Jawaban:

402

Catatan: posting ini mengasumsikan sintaksis Python 3.x.

Sebuah Generator hanyalah sebuah fungsi yang mengembalikan sebuah objek di mana Anda dapat memanggil next, sehingga untuk setiap panggilan itu mengembalikan beberapa nilai, sampai menimbulkan StopIterationpengecualian, menandakan bahwa semua nilai telah dihasilkan. Objek semacam itu disebut iterator .

Fungsi normal mengembalikan nilai tunggal menggunakan return, seperti di Jawa. Dalam Python, bagaimanapun, ada alternatif, yang disebut yield. Menggunakan yielddi mana saja dalam suatu fungsi menjadikannya generator. Perhatikan kode ini:

>>> def myGen(n):
...     yield n
...     yield n + 1
... 
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Seperti yang Anda lihat, myGen(n)adalah fungsi yang menghasilkan ndan n + 1. Setiap panggilan untuk nextmenghasilkan nilai tunggal, sampai semua nilai telah dihasilkan. forloop memanggil nextdi latar belakang, dengan demikian:

>>> for n in myGen(6):
...     print(n)
... 
6
7

Demikian juga ada ekspresi generator , yang menyediakan sarana untuk secara ringkas menggambarkan jenis umum generator tertentu:

>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Perhatikan bahwa ekspresi generator sangat mirip dengan pemahaman daftar :

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]

Perhatikan bahwa objek generator dihasilkan sekali , tetapi kode tidak dijalankan sekaligus. Hanya panggilan untuk nextbenar-benar mengeksekusi (bagian dari) kode. Eksekusi kode dalam generator berhenti begitu sebuah yieldpernyataan telah tercapai, yang padanya ia mengembalikan nilai. Panggilan berikutnya untuk nextkemudian menyebabkan eksekusi untuk melanjutkan dalam keadaan di mana generator dibiarkan setelah yang terakhir yield. Ini adalah perbedaan mendasar dengan fungsi-fungsi reguler: yang selalu memulai eksekusi di "atas" dan membuang status mereka setelah mengembalikan nilai.

Ada banyak hal yang bisa dikatakan tentang masalah ini. Misalnya dimungkinkan untuk senddata kembali ke generator ( referensi ). Tapi itu adalah sesuatu yang saya sarankan Anda tidak melihat ke dalam sampai Anda memahami konsep dasar generator.

Sekarang Anda mungkin bertanya: mengapa menggunakan generator? Ada beberapa alasan bagus:

  • Konsep tertentu dapat dideskripsikan dengan lebih ringkas menggunakan generator.
  • Alih-alih membuat fungsi yang mengembalikan daftar nilai, orang dapat menulis generator yang menghasilkan nilai dengan cepat. Ini berarti bahwa tidak ada daftar yang perlu dibangun, yang berarti bahwa kode yang dihasilkan lebih efisien memori. Dengan cara ini orang bahkan dapat menggambarkan aliran data yang hanya akan terlalu besar untuk muat dalam memori.
  • Generator memungkinkan cara alami untuk menggambarkan aliran tanpa batas . Pertimbangkan misalnya angka Fibonacci :

    >>> def fib():
    ...     a, b = 0, 1
    ...     while True:
    ...         yield a
    ...         a, b = b, a + b
    ... 
    >>> import itertools
    >>> list(itertools.islice(fib(), 10))
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

    Kode ini digunakan itertools.isliceuntuk mengambil sejumlah elemen hingga dari aliran tanpa batas. Anda disarankan untuk melihat dengan baik fungsi-fungsi dalam itertoolsmodul, karena mereka adalah alat penting untuk menulis generator yang canggih dengan sangat mudah.


   Tentang Python <= 2.6: dalam contoh di atas nextadalah fungsi yang memanggil metode __next__pada objek yang diberikan. Dalam Python <= 2.6 kita menggunakan teknik yang sedikit berbeda, yaitu o.next()alih-alih next(o). Python 2.7 memiliki next()panggilan .nextsehingga Anda tidak perlu menggunakan yang berikut ini di 2.7:

>>> g = (n for n in range(3, 5))
>>> g.next()
3
Stephan202
sumber
9
Anda menyebutkan dimungkinkan untuk senddata ke generator. Setelah Anda melakukannya, Anda memiliki 'coroutine'. Sangat sederhana untuk menerapkan pola seperti Konsumen / Produsen yang disebutkan dengan coroutine karena mereka tidak memerlukan Locks dan oleh karena itu tidak dapat menemui jalan buntu. Sulit untuk menggambarkan coroutine tanpa bashing thread, jadi saya akan mengatakan coroutine adalah alternatif yang sangat elegan untuk threading.
Jochen Ritzel
Apakah generator Python pada dasarnya mesin Turing dalam hal bagaimana fungsinya?
Api Phoenix
48

Generator secara efektif adalah fungsi yang mengembalikan (data) sebelum selesai, tetapi berhenti pada saat itu, dan Anda dapat melanjutkan fungsi pada saat itu.

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words

dan seterusnya. Manfaat (atau satu) dari generator adalah karena mereka menangani data satu per satu, Anda dapat menangani sejumlah besar data; dengan daftar, kebutuhan memori yang berlebihan bisa menjadi masalah. Generator, seperti halnya daftar, dapat diubah, sehingga dapat digunakan dengan cara yang sama:

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time

Perhatikan bahwa generator menyediakan cara lain untuk menangani ketidakterbatasan, misalnya

>>> from time import gmtime, strftime
>>> def myGen():
...     while True:
...         yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())    
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000   

Generator merangkum loop tak terbatas, tetapi ini bukan masalah karena Anda hanya mendapatkan setiap jawaban setiap kali Anda memintanya.

Caleb Hattingh
sumber
30

Pertama-tama, istilah generator pada awalnya agak tidak jelas dalam Python, yang menyebabkan banyak kebingungan. Anda mungkin maksud iterator dan iterables (lihat di sini ). Kemudian dalam Python ada juga fungsi generator (yang mengembalikan objek generator), objek generator (yang merupakan iterator) dan ekspresi generator (yang dievaluasi ke objek generator).

Menurut entri glosarium untuk generator , sepertinya terminologi resmi sekarang adalah generator yang merupakan kependekan dari "fungsi generator". Di masa lalu dokumentasi mendefinisikan istilah tidak konsisten, tetapi untungnya ini telah diperbaiki.

Mungkin masih merupakan ide yang baik untuk menjadi tepat dan menghindari istilah "generator" tanpa spesifikasi lebih lanjut.

nikow
sumber
2
Hmm saya pikir Anda benar, setidaknya menurut pengujian beberapa baris dalam Python 2.6. Ekspresi generator mengembalikan iterator (alias 'objek generator'), bukan generator.
Craig McQueen
22

Generator dapat dianggap sebagai singkatan untuk membuat iterator. Mereka berperilaku seperti Java Iterator. Contoh:

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g)   # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next()  # iterator is at the end; calling next again will throw
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Semoga ini bisa membantu / apa yang Anda cari.

Memperbarui:

Seperti banyak jawaban lain yang ditampilkan, ada berbagai cara untuk membuat generator. Anda dapat menggunakan sintaks kurung seperti dalam contoh saya di atas, atau Anda dapat menggunakan hasil. Fitur lain yang menarik adalah bahwa generator bisa "tak terbatas" - iterator yang tidak berhenti:

>>> def infinite_gen():
...     n = 0
...     while True:
...         yield n
...         n = n + 1
... 
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...
terlalu banyak berpikir
sumber
1
Sekarang, Java telah Stream s, yang jauh lebih mirip dengan generator, kecuali bahwa Anda tampaknya tidak bisa mendapatkan elemen berikutnya tanpa kerumitan yang mengejutkan.
Dana Gugatan Monica
12

Tidak ada padanan Java.

Ini sedikit contoh yang dibuat-buat:

#! /usr/bin/python
def  mygen(n):
    x = 0
    while x < n:
        x = x + 1
        if x % 3 == 0:
            yield x

for a in mygen(100):
    print a

Ada loop dalam generator yang berjalan dari 0 ke n, dan jika variabel loop adalah kelipatan 3, itu menghasilkan variabel.

Selama setiap iterasi dari forloop generator dijalankan. Jika ini adalah pertama kalinya generator dijalankan, itu dimulai di awal, jika tidak berlanjut dari waktu sebelumnya ia menghasilkan.

Wernsey
sumber
2
Paragraf terakhir sangat penting: Keadaan fungsi generator 'beku' setiap kali menghasilkan sth, dan terus dalam kondisi yang sama persis ketika dipanggil pada waktu berikutnya.
Johannes Charra
Tidak ada persamaan sintaksis di Jawa untuk "ekspresi generator", tetapi generator - setelah Anda mendapatkannya - pada dasarnya hanyalah sebuah iterator (karakteristik dasar yang sama dengan iterator Java).
overthink
@ overthink: Ya, generator dapat memiliki efek samping lain yang tidak dimiliki oleh iterator Java. Jika saya meletakkan print "hello"setelah x=x+1dalam contoh saya, "halo" akan dicetak 100 kali, sedangkan tubuh for for loop hanya akan dieksekusi 33 kali.
Wernsey
@ iWerner: Cukup yakin efek yang sama bisa didapat di Java. Implementasi next () di iterator Java yang setara masih harus mencari dari 0 hingga 99 (menggunakan mygen Anda (100) contoh), sehingga Anda bisa System.out.println () setiap kali jika Anda mau. Anda hanya akan mengembalikan 33 kali dari next (). Apa yang tidak dimiliki Java adalah sintaks hasil yang sangat berguna yang secara signifikan lebih mudah dibaca (dan ditulis).
overthink
Saya suka membaca dan mengingat def satu baris ini: Jika ini adalah pertama kalinya generator dijalankan, itu dimulai di awal, jika tidak, itu berlanjut dari waktu sebelumnya dihasilkan.
Iqra.
8

Saya suka menggambarkan generator, untuk mereka yang memiliki latar belakang yang layak dalam bahasa pemrograman dan komputasi, dalam hal susunan bingkai.

Dalam banyak bahasa, ada tumpukan di atas yang merupakan tumpukan "bingkai" saat ini. Bingkai tumpukan termasuk ruang yang dialokasikan untuk variabel lokal ke fungsi termasuk argumen yang diteruskan ke fungsi itu.

Saat Anda memanggil fungsi, titik eksekusi saat ini ("penghitung program" atau yang setara) didorong ke tumpukan, dan bingkai tumpukan baru dibuat. Eksekusi kemudian mentransfer ke awal fungsi yang dipanggil.

Dengan fungsi reguler, pada suatu titik fungsi mengembalikan nilai, dan tumpukan "muncul". Bingkai tumpukan fungsi dibuang dan eksekusi dilanjutkan di lokasi sebelumnya.

Ketika suatu fungsi adalah generator, ia dapat mengembalikan nilai tanpa bingkai tumpukan yang dibuang, menggunakan pernyataan hasil. Nilai-nilai variabel lokal dan penghitung program dalam fungsi dipertahankan. Ini memungkinkan generator untuk dilanjutkan di lain waktu, dengan eksekusi berlanjut dari pernyataan hasil, dan dapat mengeksekusi lebih banyak kode dan mengembalikan nilai lain.

Sebelum Python 2.5 ini semua generator lakukan. Python 2,5 menambahkan kemampuan untuk melewati nilai kembali di generator juga. Dengan demikian, nilai yang lewat tersedia sebagai ekspresi yang dihasilkan dari pernyataan hasil yang telah mengembalikan kontrol (dan nilai) sementara dari generator.

Keuntungan utama untuk generator adalah bahwa "keadaan" fungsi dipertahankan, tidak seperti dengan fungsi biasa di mana setiap kali bingkai tumpukan dibuang, Anda kehilangan semua "keadaan" itu. Keuntungan sekunder adalah bahwa beberapa overhead panggilan fungsi (membuat dan menghapus bingkai tumpukan) dihindari, meskipun ini biasanya merupakan keuntungan kecil.

Peter Hansen
sumber
6

Satu-satunya hal yang dapat saya tambahkan ke jawaban Stephan202 adalah rekomendasi agar Anda melihat presentasi David Beazley's PyCon '08 "Trik Generator untuk Pemrogram Sistem," yang merupakan penjelasan tunggal terbaik tentang bagaimana dan mengapa generator yang pernah saya lihat dimana saja. Ini adalah hal yang membawa saya dari "Python kelihatan menyenangkan" menjadi "Ini yang saya cari." Ada di http://www.dabeaz.com/generators/ .

Robert Rossney
sumber
6

Ini membantu untuk membuat perbedaan yang jelas antara fungsi foo, dan generator foo (n):

def foo(n):
    yield n
    yield n+1

foo adalah fungsi. foo (6) adalah objek generator.

Cara khas untuk menggunakan objek generator adalah dalam satu lingkaran:

for n in foo(6):
    print(n)

Lingkaran dicetak

# 6
# 7

Pikirkan generator sebagai fungsi yang dapat dilanjutkan.

yieldberperilaku seperti returndalam arti bahwa nilai-nilai yang dihasilkan dapat "dikembalikan" oleh generator. Tidak seperti return, bagaimanapun, pada saat generator ditanya nilai, fungsi generator, foo, melanjutkan di mana ia tinggalkan - setelah pernyataan hasil terakhir - dan terus berjalan sampai menyentuh pernyataan hasil yang lain.

Di belakang layar, ketika Anda memanggil bar=foo(6)bilah objek generator ditentukan bagi Anda untuk memiliki nextatribut.

Anda bisa menyebutnya sendiri untuk mengambil nilai yang dihasilkan dari foo:

next(bar)    # Works in Python 2.6 or Python 3.x
bar.next()   # Works in Python 2.5+, but is deprecated. Use next() if possible.

Ketika foo berakhir (dan tidak ada lagi nilai yang dihasilkan), panggilan next(bar)melempar kesalahan StopInteration.

unutbu
sumber
5

Posting ini akan menggunakan angka Fibonacci sebagai alat untuk membangun guna menjelaskan kegunaan generator Python .

Posting ini akan menampilkan kode C ++ dan Python.

Angka-angka Fibonacci didefinisikan sebagai urutan: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....

Atau secara umum:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2

Ini dapat ditransfer ke fungsi C ++ dengan sangat mudah:

size_t Fib(size_t n)
{
    //Fib(0) = 0
    if(n == 0)
        return 0;

    //Fib(1) = 1
    if(n == 1)
        return 1;

    //Fib(N) = Fib(N-2) + Fib(N-1)
    return Fib(n-2) + Fib(n-1);
}

Tetapi jika Anda ingin mencetak enam angka Fibonacci pertama, Anda akan menghitung ulang banyak nilai dengan fungsi di atas.

Misalnya :, Fib(3) = Fib(2) + Fib(1)tetapi Fib(2)juga menghitung ulang Fib(1). Semakin tinggi nilai yang ingin Anda hitung, Anda akan semakin buruk.

Jadi seseorang mungkin tergoda untuk menulis ulang di atas dengan melacak keadaan main.

// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
    int result = pp + p;
    pp = p;
    p = result;
    return result;
}

int main(int argc, char *argv[])
{
    size_t pp = 0;
    size_t p = 1;
    std::cout << "0 " << "1 ";
    for(size_t i = 0; i <= 4; ++i)
    {
        size_t fibI = GetNextFib(pp, p);
        std::cout << fibI << " ";
    }
    return 0;
}

Tapi ini sangat jelek, dan mempersulit logika kita main . Akan lebih baik untuk tidak perlu khawatir tentang keadaan di kitamain fungsi .

Kita bisa mengembalikan a vector nilai dan menggunakan nilai iteratoruntuk mengulangi set nilai itu, tetapi ini membutuhkan banyak memori sekaligus untuk sejumlah besar nilai pengembalian.

Jadi kembali ke pendekatan lama kita, apa yang terjadi jika kita ingin melakukan hal lain selain mencetak angka? Kami harus menyalin dan menempelkan seluruh blok kodemain dan mengubah pernyataan keluaran menjadi apa pun yang ingin kami lakukan. Dan jika Anda menyalin dan menempelkan kode, maka Anda harus ditembak. Anda tidak ingin tertembak, bukan?

Untuk mengatasi masalah ini, dan untuk menghindari tembakan, kami dapat menulis ulang blok kode ini menggunakan fungsi panggilan balik. Setiap kali nomor Fibonacci baru ditemukan, kami akan memanggil fungsi callback.

void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
    if(max-- == 0) return;
    FoundNewFibCallback(0);
    if(max-- == 0) return;
    FoundNewFibCallback(1);

    size_t pp = 0;
    size_t p = 1;
    for(;;)
    {
        if(max-- == 0) return;
        int result = pp + p;
        pp = p;
        p = result;
        FoundNewFibCallback(result);
    }
}

void foundNewFib(size_t fibI)
{
    std::cout << fibI << " ";
}

int main(int argc, char *argv[])
{
    GetFibNumbers(6, foundNewFib);
    return 0;
}

Ini jelas merupakan peningkatan, logika Anda main tidak berantakan, dan Anda dapat melakukan apa pun yang Anda inginkan dengan angka-angka Fibonacci, cukup tentukan panggilan balik baru.

Tapi ini masih belum sempurna. Bagaimana jika Anda hanya ingin mendapatkan dua angka Fibonacci pertama, lalu melakukan sesuatu, lalu mendapatkan lebih banyak, lalu melakukan sesuatu yang lain?

Yah, kita bisa terus seperti dulu, dan kita bisa mulai menambahkan status lagi main , memungkinkan GetFibNumbers untuk memulai dari titik arbitrer. Tapi ini akan semakin menggembungkan kode kita, dan itu sudah terlihat terlalu besar untuk tugas sederhana seperti mencetak angka Fibonacci.

Kita dapat menerapkan model produsen dan konsumen melalui beberapa utas. Tetapi ini semakin menyulitkan kode.

Sebagai gantinya mari kita bicara tentang generator.

Python memiliki fitur bahasa yang sangat bagus yang memecahkan masalah seperti ini yang disebut generator.

Generator memungkinkan Anda untuk mengeksekusi suatu fungsi, berhenti pada titik arbitrer, dan kemudian melanjutkan lagi di mana Anda tinggalkan. Setiap kali mengembalikan nilai.

Pertimbangkan kode berikut yang menggunakan generator:

def fib():
    pp, p = 0, 1
    while 1:
        yield pp
        pp, p = p, pp+p

g = fib()
for i in range(6):
    g.next()

Yang memberi kami hasil:

0 1 1 2 3 5

Itu yield pernyataan digunakan dalam conjuction dengan generator Python. Ini menyimpan status fungsi dan mengembalikan nilai yeilded. Lain kali Anda memanggil fungsi next () pada generator, ia akan melanjutkan di mana hasil ditinggalkan.

Ini jauh lebih bersih daripada kode fungsi panggilan balik. Kami memiliki kode yang lebih bersih, kode yang lebih kecil, dan belum lagi kode yang lebih fungsional (Python memungkinkan bilangan bulat besar secara sewenang-wenang).

Sumber

Brian R. Bondy
sumber
3

Saya percaya penampilan pertama iterator dan generator dalam bahasa pemrograman Icon, sekitar 20 tahun yang lalu.

Anda dapat menikmati ikhtisar Ikon , yang memungkinkan Anda membungkusnya tanpa berkonsentrasi pada sintaks (karena Ikon adalah bahasa yang Anda mungkin tidak tahu, dan Griswold menjelaskan manfaat bahasanya kepada orang-orang yang datang dari bahasa lain).

Setelah membaca hanya beberapa paragraf di sana, utilitas generator dan iterator mungkin menjadi lebih jelas.

Nosredna
sumber
2

Pengalaman dengan daftar pemahaman telah menunjukkan utilitas luas mereka di seluruh Python. Namun, banyak kasus penggunaan tidak perlu memiliki daftar lengkap yang dibuat dalam memori. Sebaliknya, mereka hanya perlu mengulangi elemen satu per satu.

Sebagai contoh, kode penjumlahan berikut akan membangun daftar kuadrat penuh dalam memori, beralih pada nilai-nilai itu, dan, ketika referensi tidak lagi diperlukan, hapus daftar:

sum([x*x for x in range(10)])

Memori disimpan dengan menggunakan ekspresi generator sebagai gantinya:

sum(x*x for x in range(10))

Manfaat serupa diberikan pada konstruktor untuk objek kontainer:

s = Set(word  for line in page  for word in line.split())
d = dict( (k, func(k)) for k in keylist)

Ekspresi generator sangat berguna dengan fungsi seperti jumlah (), min (), dan maks () yang mengurangi input yang dapat diulang ke nilai tunggal:

max(len(line)  for line in file  if line.strip())

lebih

Saqib Mujtaba
sumber
1

Saya memasang potongan kode ini yang menjelaskan 3 konsep utama tentang generator:

def numbers():
    for i in range(10):
            yield i

gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers

for i in gen: #we iterate over the generator and the values are printed
    print(i)

#the generator is now empty

for i in gen: #so this for block does not print anything
    print(i)
Stefan Iancu
sumber