Protokol iterasi

Protokol iterasi dengan iter() dan next() serta cara kerja perulangan for di balik layar.

Daftar isi

Setiap perulangan for pada Python berdiri di atas satu aturan yang seragam, yaitu protokol iterasi. Aturan ini mengatur cara sebuah objek memberikan elemennya satu per satu melalui dua fungsi bawaan, iter() dan next(), dan mengakhiri perulangan melalui StopIteration. Topik ini membahas perbedaan iterable dan iterator, mekanisme for di balik layar, sifat iterator yang hanya dapat dipakai sekali, serta pembuatan iterator sendiri dengan kelas.

Iterable dan iterator#

Sebuah iterable adalah objek yang mampu memberikan iterator, yaitu objek pembawa posisi yang menyuplai elemen satu per satu. Fungsi iter() meminta iterator dari sebuah iterable, sedangkan fungsi next() mengambil elemen berikutnya dari iterator tersebut. Apabila elemennya sudah tidak ada, pemanggilan next() menimbulkan error StopIteration sebagai sinyal akhir. List adalah iterable, tetapi list bukan iterator, karena list tidak menyimpan posisi pembacaan, sedangkan iterator yang dihasilkan iter(list) menyimpan posisi tersebut.

python Contoh: iter dan next pada list
angka = [10, 20, 30]
it = iter(angka)
print(type(it).__name__)
print(next(it))
print(next(it))
print(next(it))
try:
    next(it)
except StopIteration:
    print("StopIteration: elemen habis")
text Keluaran
list_iterator
10
20
30
StopIteration: elemen habis

Tiga panggilan next() menghasilkan tiga elemen secara berurutan, dan panggilan keempat menimbulkan StopIteration karena pasokannya sudah habis. Pada tingkat bahasa, iterable mendefinisikan metode __iter__ yang mengembalikan iterator, sedangkan iterator mendefinisikan metode __next__ untuk mengambil elemen berikutnya dan metode __iter__ yang mengembalikan dirinya sendiri. Dua metode ini yang diperiksa Python setiap kali perulangan atau fungsi iterasi lainnya bekerja.

Pemisahan peran ini membawa dua keuntungan praktis. Satu iterable dapat memproduksi banyak iterator yang berjalan sendiri-sendiri, sehingga dua perulangan atas list yang sama tidak saling menggeser posisi baca. Sebaliknya, iterator menyimpan posisi baca di dalam dirinya, sehingga keadaannya berubah setiap kali satu elemen diambil. Perbedaan inilah yang mendasari sifat sekali pakai pada iterator.

Cara kerja for di balik layar#

Perulangan for x in data memanggil iter(data) satu kali di awal, lalu memanggil next(it) berulang kali, menjalankan badan perulangan dengan nilai yang dikembalikan, dan berhenti begitu StopIteration muncul. Tidak ada mekanisme tersembunyi lainnya, sehingga perilaku yang sama dapat ditulis ulang secara eksplisit dengan while dan try. Diagram berikut merangkum alurnya.

for x in datait = iter(data)next(it)badan perulangan berjalan dengan xyaada nilai?tidakStopIterationperulangan berakhir
Cara kerja perulangan for di balik layar
python Contoh: bentuk eksplisit dari for
def manual_for(iterable, aksi):
    it = iter(iterable)
    while True:
        try:
            item = next(it)
        except StopIteration:
            break
        aksi(item)


def cetak(x):
    print(f"elemen: {x}")


manual_for([10, 20, 30], cetak)
text Keluaran
elemen: 10
elemen: 20
elemen: 30

Fungsi manual_for di atas menghasilkan cetakan yang sama persis dengan for x in [10, 20, 30]: print(x). Pemahaman ini berguna ketika sebuah objek gagal diiterasi, karena pesan errornya dapat ditelusuri ke salah satu langkah protokol, misalnya TypeError yang ditimbulkan iter() terhadap objek tanpa metode __iter__. Struktur data apa pun yang menyediakan __iter__, termasuk dictionary, set, berkas teks, dan generator, otomatis dapat dipakai dalam for.

Iterator hanya dapat dipakai sekali#

Iterator menyimpan posisi pembacaan dan posisi itu tidak pernah berbalik ke awal. Setelah elemen terakhir diambil, iterator berada dalam keadaan habis dan setiap pembacaan berikutnya menimbulkan StopIteration, sekalipun objek sumbernya masih berisi data. Sifat ini berbeda dari iterable seperti list, yang dapat memproduksi iterator baru setiap kali iter() dipanggil.

python Contoh: iterator yang sudah habis
huruf = iter(["x", "y"])
print(list(huruf))
print(list(huruf))
print(iter(huruf) is huruf)
text Keluaran
['x', 'y']
[]
True

Pemanggilan list() pertama memuat kedua elemen, sedangkan pemanggilan kedua menghasilkan list kosong karena iteratornya sudah habis. Baris terakhir menunjukkan bahwa iter() atas sebuah iterator mengembalikan objek yang sama, sehingga for yang dijalankan atas iterator yang sudah habis tidak berjalan satu putaran pun. Apabila data perlu diiterasi lebih dari sekali, simpanlah sumbernya sebagai list dan panggil iter() atau for atas list tersebut setiap kali, bukan atas iterator lamanya.

Konsekuensi lain dari posisi yang hanya bergerak maju adalah tidak adanya dukungan indeks. Penulisan it[0] atas iterator menimbulkan error TypeError, begitu juga slicing, karena iterator tidak menyimpan kumpulannya melainkan hanya posisi pembacaannya. Fungsi konsumsi seperti sum() dan any() justru dirancang untuk bentuk ini, karena keduanya menghabiskan aliran sekali lewat tanpa membutuhkan akses acak.

Membuat iterator sendiri#

Kelas apa pun yang mendefinisikan __iter__ dan __next__ mengikuti protokol iterasi dan dapat langsung dipakai dalam for. Metode __iter__ mengembalikan objek iterator, sedangkan __next__ mengembalikan elemen berikutnya atau menimbulkan StopIteration bila pasokan selesai. Contoh berikut berupa kelas penghitung mundur yang menyimpan keadaannya sendiri.

python hitung-mundur.py
class HitungMundur:
    def __init__(self, mulai):
        self.nilai = mulai

    def __iter__(self):
        return self

    def __next__(self):
        if self.nilai <= 0:
            raise StopIteration
        self.nilai -= 1
        return self.nilai + 1


for n in HitungMundur(3):
    print(n)
text Keluaran
3
2
1

Perulangan for hanya memeriksa protokolnya, sehingga objek dari kelas yang didefinisikan sendiri diperlakukan sama seperti objek bawaan. Setiap putaran memanggil __next__, yang menurunkan self.nilai dan mengembalikan hitungan saat itu, hingga nilainya mencapai nol dan StopIteration ditimbulkan. Perlu dicatat bahwa __iter__ di sini mengembalikan self, sehingga satu instans HitungMundur adalah iterator untuk dirinya sendiri dan hanya dapat dipakai sekali, sebagaimana perilaku iterator pada umumnya.

Apabila kelas hanya dimaksudkan sebagai iterable dan bukan iterator, metode __iter__ boleh mengembalikan iterator baru setiap kali dipanggil, misalnya iterator atas data yang disimpannya, sehingga satu instans dapat diiterasi berkali-kali. Bentuk kelas di atas tetap menjadi rujukan karena memperlihatkan kedua metode protokol secara utuh. Fungsi generator menyusun iterator dengan mekanisme yang jauh lebih ringkas, tetapi kontrak yang dipenuhinya tetap sama.

Kesalahan umum#

Kesalahan pertama adalah mengubah ukuran container selama iterasinya berjalan, misalnya memanggil pop() pada dictionary yang sedang diiterasi. Python mendeteksi perubahan ini dan menimbulkan error RuntimeError alih-alih melompati elemen secara diam-diam.

python Contoh: mengubah dictionary selama iterasi
stok = {"buku": 3, "pensil": 5}
try:
    for k in stok:
        stok.pop(k)
except RuntimeError as e:
    print(type(e).__name__, str(e))
text Keluaran
RuntimeError dictionary changed size during iteration

Perbaikannya adalah mengiterasi salinan kuncinya, misalnya for k in list(stok):, sehingga penghapusan dilakukan atas dictionary tanpa mengganggu urutan yang sedang dibaca. Kesalahan kedua adalah memanggil next() pada iterator yang sudah habis tanpa menangkap StopIteration. Error itu memang sinyal akhir yang dirancang untuk perulangan, tetapi pada kode manual pemanggilannya perlu dibungkus try, atau dipanggil lewat next(it, nilai_default) yang mengembalikan nilai default alih-alih menimbulkan error. Kesalahan ketiga adalah berharap iterator mengulang dari awal pada pemakaian kedua, dan perbaikannya sudah dibahas pada bagian sebelumnya, yaitu menyimpan sumber datanya dan membuat iterator baru setiap kali.

Ringkasan#

  • Iterable menyediakan iterator melalui iter(), sedangkan iterator menyuplai elemen lewat next() dan menimbulkan StopIteration ketika habis.
  • Perulangan for memanggil iter() satu kali dan next() berulang kali, lalu berhenti begitu StopIteration muncul.
  • Iterator hanya dapat dipakai sekali, dan iter() atas iterator mengembalikan dirinya sendiri, sehingga iterator habis tetap habis.
  • Kelas apa pun dengan __iter__ dan __next__ dapat diiterasi seperti objek bawaan mana pun.

Referensi#