Ekspresi generator adalah bentuk ringkas untuk membuat generator dalam satu baris, ditulis seperti comprehension tetapi dengan tanda kurung biasa, dan nilainya tidak dihitung sampai diminta. Sifat penundaan ini disebut laziness, yaitu evaluasi yang tertunda hingga hasilnya benar-benar dibutuhkan. Topik ini membahas sintaksnya, konsekuensi sifat sekali pakai, perilaku memorinya, dan situasi ketika laziness lebih unggul daripada membangun list terlebih dahulu.
Sintaks ekspresi generator#
Ekspresi generator ditulis sebagai (ekspresi for variabel in iterable), dengan susunan klausa yang sama persis dengan list comprehension. Perbedaannya hanya pada tanda kurung, dan perbedaan kecil itu mengubah seluruh perilaku, karena kurung siku membangun list secara langsung, sedangkan kurung biasa menghasilkan objek generator yang menghitung nilainya saat diiterasi.
data = [3, 1, 4, 1, 5]
g = (n * 2 for n in data)
print(type(g).__name__)
print(list(g))
daftar = [n * 2 for n in data]
print(daftar)generator
[6, 2, 8, 2, 10]
[6, 2, 8, 2, 10]Kedua bentuk menghasilkan nilai yang sama apabila dilihat dari hasil list() dan cetakannya, tetapi objek yang dipegangnya berbeda. Variabel g menunjuk generator yang belum menghitung apa pun, sedangkan variabel daftar menunjuk list yang seluruh elemennya sudah tersusun. Ketika ekspresi generator menjadi satu-satunya argumen sebuah fungsi, satu pasang tanda kurungnya boleh dilepas, sehingga sum(n * n for n in data) ditulis tanpa kurung ganda.
Kelonggaran itu hanya berlaku pada posisi argumen tunggal. Apabila ada argumen lain pada pemanggilan yang sama, tanda kurung generatornya wajib ditulis penuh, misalnya sum((n for n in data), 100) untuk menjumlahkan hasil aliran dengan nilai awal 100. Dua aturan kecil ini menutup salah baca yang paling sering terjadi saat membaca kode yang memakai ekspresi generator.
Evaluasi lazy dan sifat sekali pakai#
Sebuah generator ekspresi tidak mengeksekusi apa pun pada saat dibuat, dan setiap elemen dihitung tepat satu kali pada saat diminta. Sifat ini dapat diperiksa dengan fungsi kecil yang mencatat setiap kali ekspresinya berjalan, karena banyaknya catatan menunjukkan berapa banyak elemen yang benar-benar sudah dihitung.
tereksekusi = []
def catat(n):
tereksekusi.append(n)
return n
g = (catat(n) for n in range(5))
print("sebelum iterasi:", len(tereksekusi))
next(g)
next(g)
print("dua panggilan next:", len(tereksekusi))
print(list(g))
print("setelah habis:", len(tereksekusi))sebelum iterasi: 0
dua panggilan next: 2
[2, 3, 4]
setelah habis: 5Pembuatan generator tidak menghitung apa pun, dua panggilan next() menghitung dua elemen, dan list() melengkapi sisanya hingga lima. Konsekuensi pentingnya, sekuens ini hanya mengalir sekali, karena posisi baca yang sama tidak pernah berbalik. Setelah seluruh elemen dikonsumsi, generatornya kosong dan setiap pembacaan berikutnya menghasilkan nilai kosong tanpa error apa pun.
Sifat tertunda ini juga berarti pembuatan generator atas sumber besar hanya membutuhkan usaha yang tetap, karena yang disusun hanyalah ekspresinya dan iterator sumbernya. Perhitungan yang mahal tetap harus dibayar, tetapi dibayar hanya untuk elemen yang benar-benar diminta. Inilah titik temu antara laziness dan fungsi yang berhenti lebih awal, yang dibahas pada bagian akhir topik ini.
g = (n for n in range(4))
print(sum(g))
print(sum(g))6
0Perilaku memori#
Perbedaan penyimpanan antara list dan generator ekspresi terlihat pada data yang besar. List menyimpan seluruh elemennya di memori, sedangkan generator hanya menyimpan ekspresinya, iterator sumbernya, dan satu elemen yang sedang dipegang. Ukuran byte persisnya bergantung pada versi Python, sehingga contoh di bawah membandingkan urutan besarannya saja, yaitu apakah list yang memuat sepuluh juta elemen memakan memori lebih besar daripada generator atas rentang yang sama.
import sys
jumlah = 10_000_000
daftar = [n for n in range(jumlah)]
aliran = (n for n in range(jumlah))
print(sys.getsizeof(daftar) > sys.getsizeof(aliran))TruePernyataan pertama harus membangun list berisi sepuluh juta elemen sebelum baris berikutnya berjalan, sedangkan pernyataan kedua hanya menyusun rencana perhitungan. Karena itu generator bukan pengganti list untuk segala keadaan, melainkan pilihan ketika data hanya dilalui sekali dan tidak perlu diindeks. Nilai di tengah tidak dapat diambil kembali dari generator, dan len() juga tidak berlaku karena banyaknya elemen memang belum diketahui.
Kapan laziness lebih unggul#
Laziness memberi keuntungan nyata ketika hasil hanya sebagian yang dibutuhkan atau ketika rantai pemrosesan menjadi panjang. Fungsi sum() pada contoh berikut menjumlahkan hasil dari penyaringan dan pengubahan dalam satu aliran, tanpa membuat list perantara untuk hasil penyaringannya.
nilai = [12, 7, 25, 4, 18, 9, 30]
total = sum(n * 3 for n in nilai if n % 2 == 0)
print(total)192Keuntungan yang lebih besar muncul pada fungsi pengujian seperti any() dan all(), karena keduanya berhenti begitu jawabannya pasti. Dengan generator, elemen-elemen setelah jawaban itu tidak pernah dihitung, sedangkan versi list tetap harus membangun seluruh daftarnya terlebih dahulu.
def ada_genap(jumlah_data):
return any(n % 2 == 0 for n in range(jumlah_data))
print(ada_genap(10_000_000))TrueAngka nol genap ditemukan pada elemen pertama, sehingga sisa rentang sepuluh juta angka tidak disentuh sama sekali. Pola inilah yang membuat generator cocok untuk sumber data besar, berkas berukuran raksasa, dan rantai penyaringan yang panjang, selama pemrosesannya bergerak maju dari awal ke akhir. Fungsi all() menampilkan sifat yang sama pada arah sebaliknya, yaitu berhenti begitu satu elemen gagal uji, sehingga pasangan generator dan fungsi konsumsi ini saling menguatkan.
Kesalahan umum#
Kesalahan pertama adalah membuat generator ekspresi dari iterator yang sudah habis. Ekspresi yang ditulis tetap sah, tetapi iterator sumber tidak memiliki elemen tersisa, sehingga hasil kosong ini sering disangka berasal dari kesalahan lain.
it = iter([1, 2, 3])
print(list(it))
g = (n * 10 for n in it)
print(list(g))[1, 2, 3]
[]Perbaikannya adalah membuat ulang iterator sumbernya, misalnya dari list aslinya, sebelum generator dibangun. Kesalahan kedua adalah memanggil len() atas generator ekspresi. Jumlah elemennya memang belum ada sebelum iterasi dijalankan, sehingga error TypeError muncul.
try:
len(n for n in range(3))
except TypeError as e:
print(type(e).__name__, str(e))TypeError object of type 'generator' has no len()Kebutuhan seperti ini menunjukkan bahwa data sebaiknya berupa list, atau dihitung jumlahnya lewat sum(1 for n in ...) bila datanya memang besar. Aturan praktisnya sederhana, yaitu menggunakan list bila perlu indeks, panjang, atau iterasi berulang, dan menggunakan generator bila data hanya mengalir sekali.
Ringkasan#
- Ekspresi generator memakai tanda kurung dengan klausa comprehension, dan menghasilkan objek generator yang evaluasinya tertunda.
- Generator hanya menghitung elemen saat diminta, mengalir sekali, dan menjadi kosong setelah dikonsumsi.
- List menyimpan seluruh elemen, sedangkan generator hanya menyimpan keadaannya, sehingga rentang besar lebih hemat memori sebagai generator.
- Laziness unggul pada pemrosesan sekali lewat, rantai penyaringan, dan fungsi seperti
any()yang berhenti lebih awal.
Referensi#
- Generator expressions — Python Software Foundation — Referensi bahasa untuk sintaks dan semantik generator ekspresi.
- PEP 289 — Python Software Foundation — Usulan resmi yang memperkenalkan generator ekspresi ke Python.
- Functional Programming HOWTO — Python Software Foundation — Panduan resmi yang membahas laziness dan iterator secara menyeluruh.