Untuk apa Anda menggunakan fungsi generator Python?

213

Saya mulai belajar Python dan saya menemukan fungsi generator, yang memiliki pernyataan hasil di dalamnya. Saya ingin tahu jenis masalah apa yang fungsi-fungsi ini benar-benar pecahkan.

quamrana
sumber
6
mungkin pertanyaan yang lebih baik adalah ketika kita seharusnya tidak menggunakan 'em
cregox
1
Contoh dunia nyata di sini
Giri

Jawaban:

239

Generator memberi Anda evaluasi malas. Anda menggunakannya dengan mengulanginya, baik secara eksplisit dengan 'untuk' atau secara implisit dengan meneruskannya ke fungsi apa pun atau membangun yang diulanginya. Anda dapat menganggap generator sebagai mengembalikan beberapa item, seolah-olah mereka mengembalikan daftar, tetapi alih-alih mengembalikannya sekaligus, mereka mengembalikannya satu per satu, dan fungsi generator dijeda hingga item berikutnya diminta.

Generator bagus untuk menghitung set besar hasil (khususnya perhitungan yang melibatkan loop sendiri) di mana Anda tidak tahu apakah Anda akan membutuhkan semua hasil, atau di mana Anda tidak ingin mengalokasikan memori untuk semua hasil pada saat yang sama . Atau untuk situasi di mana generator menggunakan generator lain , atau mengkonsumsi sumber daya lain, dan itu lebih nyaman jika itu terjadi selambat mungkin.

Penggunaan lain untuk generator (yang benar-benar sama) adalah untuk mengganti panggilan balik dengan iterasi. Dalam beberapa situasi Anda ingin fungsi melakukan banyak pekerjaan dan sesekali melaporkan kembali ke pemanggil. Secara tradisional Anda akan menggunakan fungsi panggilan balik untuk ini. Anda meneruskan panggilan balik ini ke fungsi-kerja dan secara berkala akan memanggil panggilan balik ini. Pendekatan generator adalah bahwa fungsi-kerja (sekarang generator) tidak tahu apa-apa tentang panggilan balik, dan hanya menghasilkan kapan pun ia ingin melaporkan sesuatu. Penelepon, alih-alih menulis callback terpisah dan meneruskannya ke fungsi-fungsi, melakukan semua pelaporan dengan sedikit putaran 'untuk' di sekitar generator.

Misalnya, Anda menulis program 'pencarian sistem file'. Anda dapat melakukan pencarian secara keseluruhan, mengumpulkan hasil dan kemudian menampilkannya satu per satu. Semua hasil harus dikumpulkan sebelum Anda menunjukkan yang pertama, dan semua hasil akan di memori pada saat yang sama. Atau Anda bisa menampilkan hasilnya saat Anda menemukannya, yang akan lebih hemat memori dan lebih ramah terhadap pengguna. Yang terakhir dapat dilakukan dengan melewatkan fungsi pencetakan hasil ke fungsi pencarian sistem file, atau bisa dilakukan dengan hanya membuat fungsi pencarian generator dan mengulangi hasilnya.

Jika Anda ingin melihat contoh dari dua pendekatan terakhir, lihat os.path.walk () (fungsi berjalan sistem file lama dengan callback) dan os.walk () (generator berjalan sistem file baru.) Tentu saja, jika Anda benar-benar ingin mengumpulkan semua hasil dalam daftar, pendekatan generator sepele untuk dikonversi ke pendekatan daftar besar:

big_list = list(the_generator)
Thomas Wouters
sumber
Apakah generator seperti yang menghasilkan daftar filesystem melakukan tindakan secara paralel dengan kode yang menjalankan generator itu dalam satu lingkaran? Idealnya komputer akan menjalankan body of the loop (memproses hasil terakhir) sambil melakukan apa pun yang harus dilakukan generator untuk mendapatkan nilai berikutnya.
Steven Lu
@ StevenLu: Kecuali jika ada masalah untuk meluncurkan utas secara manual sebelum yielddan joinsesudahnya untuk mendapatkan hasil selanjutnya, ia tidak mengeksekusi secara paralel (dan tidak ada generator perpustakaan standar yang melakukan ini; diam-diam meluncurkan utas tidak disukai). Generator berhenti di masing-masing yieldhingga nilai berikutnya diminta. Jika generator membungkus I / O, OS mungkin secara proaktif menyimpan data dari file dengan asumsi itu akan diminta segera, tapi itu OS, Python tidak terlibat.
ShadowRanger
90

Salah satu alasan untuk menggunakan generator adalah untuk membuat solusi lebih jelas untuk beberapa jenis solusi.

Yang lain adalah memperlakukan hasil satu per satu, menghindari membuat daftar hasil yang sangat besar yang akan Anda proses pisahkan.

Jika Anda memiliki fungsi fibonacci-up-to-n seperti ini:

# function version
def fibon(n):
    a = b = 1
    result = []
    for i in xrange(n):
        result.append(a)
        a, b = b, a + b
    return result

Anda dapat lebih mudah menulis fungsi karena ini:

# generator version
def fibon(n):
    a = b = 1
    for i in xrange(n):
        yield a
        a, b = b, a + b

Fungsinya lebih jelas. Dan jika Anda menggunakan fungsi seperti ini:

for x in fibon(1000000):
    print x,

dalam contoh ini, jika menggunakan versi generator, seluruh daftar item 1000000 tidak akan dibuat sama sekali, hanya satu nilai pada suatu waktu. Itu tidak akan menjadi kasus ketika menggunakan versi daftar, di mana daftar akan dibuat terlebih dahulu.

nosklo
sumber
18
dan jika Anda memerlukan daftar, Anda selalu dapat melakukannyalist(fibon(5))
endolith
41

Lihat bagian "Motivasi" di PEP 255 .

Penggunaan generator yang tidak jelas adalah menciptakan fungsi yang dapat terputus, yang memungkinkan Anda melakukan hal-hal seperti memperbarui UI atau menjalankan beberapa pekerjaan "secara bersamaan" (disisipkan, sebenarnya) tanpa menggunakan utas.

Nickolay
sumber
1
Bagian Motivasi bagus karena memiliki contoh spesifik: "Ketika fungsi produsen memiliki pekerjaan yang cukup sulit sehingga memerlukan mempertahankan keadaan di antara nilai-nilai yang dihasilkan, sebagian besar bahasa pemrograman tidak menawarkan solusi yang menyenangkan dan efisien selain menambahkan fungsi panggilan balik ke argumen produsen. daftar ... Misalnya, tokenize.py di perpustakaan standar mengambil pendekatan ini "
Ben Creasy
38

Saya menemukan penjelasan ini yang menghilangkan keraguan saya. Karena ada kemungkinan orang yang tidak tahu Generatorsjuga tidak tahuyield

Kembali

Pernyataan pengembalian adalah tempat semua variabel lokal dihancurkan dan nilai yang dihasilkan dikembalikan (dikembalikan) ke pemanggil. Jika fungsi yang sama dipanggil beberapa waktu kemudian, fungsi tersebut akan mendapatkan serangkaian variabel baru.

Menghasilkan

Tetapi bagaimana jika variabel lokal tidak dibuang ketika kita keluar dari suatu fungsi? Ini menyiratkan bahwa kita bisa di resume the functionmana kita tinggalkan. Di sinilah konsep generatorsdiperkenalkan dan yieldpernyataan dilanjutkan di mana yang functionditinggalkan.

  def generate_integers(N):
    for i in xrange(N):
    yield i

    In [1]: gen = generate_integers(3)
    In [2]: gen
    <generator object at 0x8117f90>
    In [3]: gen.next()
    0
    In [4]: gen.next()
    1
    In [5]: gen.next()

Jadi itulah perbedaan antara returndan yieldpernyataan dalam Python.

Pernyataan hasil adalah apa yang membuat fungsi menjadi fungsi generator.

Jadi generator adalah alat sederhana dan kuat untuk membuat iterator. Mereka ditulis seperti fungsi biasa, tetapi mereka menggunakan yieldpernyataan kapan pun mereka ingin mengembalikan data. Setiap kali next () dipanggil, generator melanjutkan di tempat yang ditinggalkannya (ia mengingat semua nilai data dan pernyataan mana yang terakhir dieksekusi).

Fatamorgana
sumber
33

Contoh Dunia Nyata

Katakanlah Anda memiliki 100 juta domain di tabel MySQL Anda, dan Anda ingin memperbarui peringkat Alexa untuk setiap domain.

Hal pertama yang Anda butuhkan adalah memilih nama domain Anda dari database.

Katakanlah nama tabel Anda adalah domainsdan nama kolom adalah domain.

Jika Anda menggunakannya SELECT domain FROM domainsakan mengembalikan 100 juta baris yang akan menghabiskan banyak memori. Jadi server Anda mungkin macet.

Jadi Anda memutuskan untuk menjalankan program dalam batch. Katakanlah ukuran batch kami adalah 1000.

Dalam batch pertama kami, kami akan meminta 1000 baris pertama, memeriksa peringkat Alexa untuk setiap domain dan memperbarui baris database.

Dalam batch kedua kami, kami akan bekerja pada 1000 baris berikutnya. Dalam batch ketiga kami akan dari 2001 hingga 3000 dan seterusnya.

Sekarang kita membutuhkan fungsi generator yang menghasilkan batch kita.

Inilah fungsi generator kami:

def ResultGenerator(cursor, batchsize=1000):
    while True:
        results = cursor.fetchmany(batchsize)
        if not results:
            break
        for result in results:
            yield result

Seperti yang Anda lihat, fungsi kami menyimpan yieldhasilnya. Jika Anda menggunakan kata kunci returnalih-alih yield, maka seluruh fungsi akan berakhir setelah mencapai kembali.

return - returns only once
yield - returns multiple times

Jika suatu fungsi menggunakan kata kunci yieldmaka itu adalah generator.

Sekarang Anda dapat mengulangi seperti ini:

db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
    doSomethingWith(result)
db.close()
Giri
sumber
itu akan lebih praktis, jika hasil dapat dijelaskan dalam hal pemrograman rekursif / dyanmic!
igaurav
27

Buffering. Ketika efisien untuk mengambil data dalam potongan besar, tetapi memprosesnya dalam potongan kecil, maka generator mungkin membantu:

def bufferedFetch():
  while True:
     buffer = getBigChunkOfData()
     # insert some code to break on 'end of data'
     for i in buffer:    
          yield i

Di atas memungkinkan Anda dengan mudah memisahkan buffering dari pemrosesan. Fungsi konsumen sekarang bisa mendapatkan nilai satu per satu tanpa khawatir tentang buffering.

Rafał Dowgird
sumber
3
Jika getBigChuckOfData tidak malas, maka saya tidak mengerti apa manfaat yang didapat di sini. Apa itu use case untuk fungsi ini?
Sean Geoffrey Pietz
1
Tapi intinya adalah bahwa, IIUC, bufferedFetch adalah pemanggilan panggilan untuk getBigChunkOfData. Jika getBigChunkOfData sudah malas, maka bufferedFetch akan sia-sia. Setiap panggilan ke bufferedFetch () akan mengembalikan satu elemen buffer, meskipun BigChunk sudah dibaca. Dan Anda tidak perlu secara eksplisit menyimpan hitungan elemen berikutnya untuk kembali, karena mekanisme hasil melakukan hal itu secara implisit.
hmijail meratapi orang-orang yang mengundurkan diri
21

Saya telah menemukan bahwa generator sangat membantu dalam membersihkan kode Anda dan dengan memberi Anda cara yang sangat unik untuk merangkum dan memodulasi kode. Dalam situasi di mana Anda perlu sesuatu untuk terus memuntahkan nilai-nilai berdasarkan proses internal sendiri dan ketika bahwa kebutuhan sesuatu yang disebut dari mana saja di kode Anda (dan bukan hanya dalam loop atau blok misalnya), generator yang fitur untuk menggunakan.

Contoh abstrak akan menjadi penghasil angka Fibonacci yang tidak hidup dalam satu lingkaran dan ketika dipanggil dari mana saja akan selalu mengembalikan angka berikutnya dalam urutan:

def fib():
    first = 0
    second = 1
    yield first
    yield second

    while 1:
        next = first + second
        yield next
        first = second
        second = next

fibgen1 = fib()
fibgen2 = fib()

Sekarang Anda memiliki dua objek penghasil angka Fibonacci yang dapat Anda panggil dari mana saja dalam kode Anda dan mereka akan selalu mengembalikan angka Fibonacci yang lebih besar secara berurutan sebagai berikut:

>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5

Hal yang indah tentang generator adalah bahwa mereka merangkum keadaan tanpa harus melalui lingkaran menciptakan objek. Salah satu cara berpikir tentang mereka adalah sebagai "fungsi" yang mengingat keadaan internal mereka.

Saya mendapatkan contoh Fibonacci dari Python Generator - Apa itu? dan dengan sedikit imajinasi, Anda dapat menemukan banyak situasi lain di mana generator membuat alternatif yang bagus untuk forloop dan konstruksi iterasi tradisional lainnya.

Andz
sumber
19

Penjelasan sederhana: Pertimbangkan sebuah forpernyataan

for item in iterable:
   do_stuff()

Banyak waktu, semua item di iterabletidak perlu ada di sana sejak awal, tetapi dapat dihasilkan dengan cepat sesuai kebutuhan. Ini bisa menjadi jauh lebih efisien di keduanya

  • space (Anda tidak perlu menyimpan semua item secara bersamaan) dan
  • waktu (iterasi dapat selesai sebelum semua item diperlukan).

Di lain waktu, Anda bahkan tidak tahu semua item sebelumnya. Sebagai contoh:

for command in user_input():
   do_stuff_with(command)

Anda tidak memiliki cara untuk mengetahui semua perintah pengguna sebelumnya, tetapi Anda dapat menggunakan loop yang bagus seperti ini jika Anda memiliki generator yang memberi Anda perintah:

def user_input():
    while True:
        wait_for_command()
        cmd = get_command()
        yield cmd

Dengan generator Anda juga dapat memiliki iterasi lebih dari urutan yang tak terbatas, yang tentu saja tidak mungkin ketika iterasi di atas kontainer.

dF.
sumber
... dan urutan yang tak terbatas dapat dihasilkan dengan berulang-ulang memutar daftar kecil, kembali ke awal setelah akhir tercapai. Saya menggunakan ini untuk memilih warna dalam grafik, atau menghasilkan throbbers atau pemintal yang sibuk dalam teks.
Andrej Panjkov
@mapap: Ada itertooluntuk itu - lihat cycles.
martineau
12

Penggunaan favorit saya adalah "filter" dan "kurangi" operasi.

Katakanlah kita sedang membaca file, dan hanya ingin baris yang dimulai dengan "##".

def filter2sharps( aSequence ):
    for l in aSequence:
        if l.startswith("##"):
            yield l

Kita kemudian dapat menggunakan fungsi generator dalam loop yang tepat

source= file( ... )
for line in filter2sharps( source.readlines() ):
    print line
source.close()

Contoh pengurangannya serupa. Katakanlah kita memiliki file di mana kita perlu menemukan blok <Location>...</Location>garis. [Bukan tag HTML, tapi garis yang terlihat seperti tag.]

def reduceLocation( aSequence ):
    keep= False
    block= None
    for line in aSequence:
        if line.startswith("</Location"):
            block.append( line )
            yield block
            block= None
            keep= False
        elif line.startsWith("<Location"):
            block= [ line ]
            keep= True
        elif keep:
            block.append( line )
        else:
            pass
    if block is not None:
        yield block # A partial block, icky

Sekali lagi, kita bisa menggunakan generator ini untuk loop yang tepat.

source = file( ... )
for b in reduceLocation( source.readlines() ):
    print b
source.close()

Idenya adalah bahwa fungsi generator memungkinkan kita untuk menyaring atau mengurangi urutan, menghasilkan urutan lain satu nilai pada satu waktu.

S.Lott
sumber
8
fileobj.readlines()akan membaca seluruh file ke daftar di memori, mengalahkan tujuan menggunakan generator. Karena objek file sudah dapat diubah, Anda dapat menggunakannya for b in your_generator(fileobject):. Dengan begitu file Anda akan dibaca satu baris pada satu waktu, untuk menghindari membaca seluruh file.
nosklo
MengurangiLokasi cukup aneh menghasilkan daftar, mengapa tidak hanya menghasilkan setiap baris? Juga filter dan kurangi adalah builtin dengan perilaku yang diharapkan (lihat bantuan di ipython dll.), Penggunaan "pengurangan" Anda sama dengan filter.
James Antill
Poin bagus di readlines (). Saya biasanya menyadari bahwa file adalah iterator garis kelas satu selama pengujian unit.
S.Lott
Sebenarnya, "reduksi" menggabungkan beberapa garis individu menjadi objek komposit. Oke, ini daftar, tapi itu masih pengurangan yang diambil dari sumbernya.
S.Lott
9

Contoh praktis di mana Anda dapat menggunakan generator adalah jika Anda memiliki semacam bentuk dan Anda ingin beralih di sudut, tepi atau apa pun. Untuk proyek saya sendiri (kode sumber di sini ) saya memiliki sebuah persegi panjang:

class Rect():

    def __init__(self, x, y, width, height):
        self.l_top  = (x, y)
        self.r_top  = (x+width, y)
        self.r_bot  = (x+width, y+height)
        self.l_bot  = (x, y+height)

    def __iter__(self):
        yield self.l_top
        yield self.r_top
        yield self.r_bot
        yield self.l_bot

Sekarang saya bisa membuat persegi panjang dan loop di sudut-sudutnya:

myrect=Rect(50, 50, 100, 100)
for corner in myrect:
    print(corner)

Alih-alih __iter__Anda bisa memiliki metode iter_cornersdan menyebutnya dengan for corner in myrect.iter_corners(). Hanya saja lebih elegan untuk digunakan __iter__karena kita bisa menggunakan nama instance kelas secara langsung dalam forekspresi.

Pithikos
sumber
Saya mengagumi gagasan untuk meneruskan bidang kelas yang sama sebagai generator
eusoubrasileiro
7

Pada dasarnya menghindari fungsi-fungsi panggilan balik ketika iterating atas input mempertahankan status.

Lihat di sini dan di sini untuk ikhtisar tentang apa yang dapat dilakukan dengan menggunakan generator.

MvdD
sumber
4

Namun, beberapa jawaban yang bagus di sini, saya juga merekomendasikan pembacaan lengkap tutorial Pemrograman Fungsional Python yang membantu menjelaskan beberapa kasus penggunaan generator yang lebih kuat.

songololo
sumber
3

Karena metode pengiriman generator belum disebutkan, berikut adalah contohnya:

def test():
    for i in xrange(5):
        val = yield
        print(val)

t = test()

# Proceed to 'yield' statement
next(t)

# Send value to yield
t.send(1)
t.send('2')
t.send([3])

Ini menunjukkan kemungkinan untuk mengirim nilai ke generator yang sedang berjalan. Kursus yang lebih maju tentang generator dalam video di bawah ini (termasukyield dari eksplorasi, generator untuk pemrosesan paralel, lolos dari batas rekursi, dll.)

David Beazley menggunakan generator di PyCon 2014

John Damen
sumber
2

Saya menggunakan generator ketika server web kami bertindak sebagai proxy:

  1. Klien meminta url proksi dari server
  2. Server mulai memuat url target
  3. Server menghasilkan untuk mengembalikan hasil ke klien segera setelah mendapatkannya
Brian
sumber
1

Tumpukan barang. Kapan saja Anda ingin membuat urutan item, tetapi tidak ingin harus 'mematerialisasikan' semuanya menjadi daftar sekaligus. Misalnya, Anda dapat memiliki generator sederhana yang mengembalikan bilangan prima:

def primes():
    primes_found = set()
    primes_found.add(2)
    yield 2
    for i in itertools.count(1):
        candidate = i * 2 + 1
        if not all(candidate % prime for prime in primes_found):
            primes_found.add(candidate)
            yield candidate

Anda kemudian dapat menggunakannya untuk menghasilkan produk dari bilangan prima berikutnya:

def prime_products():
    primeiter = primes()
    prev = primeiter.next()
    for prime in primeiter:
        yield prime * prev
        prev = prime

Ini adalah contoh yang cukup sepele, tetapi Anda dapat melihat bagaimana hal itu berguna untuk memproses dataset besar (berpotensi tak terbatas!) Tanpa membuatnya terlebih dahulu, yang hanya salah satu kegunaan yang lebih jelas.

Nick Johnson
sumber
jika tidak ada (kandidat% prime untuk prime di primes_found) seharusnya jika semua (kandidat% prime untuk prime di primes_found)
rjmunro
Ya, saya bermaksud menulis "jika tidak ada (kandidat% prime == 0 untuk prime di primes_found). Namun, Anda sedikit lebih rapi. :)
Nick Johnson
Saya kira Anda lupa menghapus 'tidak' dari jika tidak semua (kandidat% prime untuk prime di primes_found)
Thava
0

Juga bagus untuk mencetak bilangan prima hingga n:

def genprime(n=10):
    for num in range(3, n+1):
        for factor in range(2, num):
            if num%factor == 0:
                break
        else:
            yield(num)

for prime_num in genprime(100):
    print(prime_num)
Sébastien Wieckowski
sumber