2.31. Iterator dan generator

Loop for melakukan lebih banyak pekerjaan dari yang terlihat. Halaman ini membahas protokol iterator yang mendasarinya, dan kata kunci yield yang memungkinkan Anda membuat iterator sendiri.

2.31.1. Protokol iterator

Setiap objek yang dapat diiterasi mengimplementasikan dua metode:

  • __iter__() -- mengembalikan sebuah iterator atas item-item objek.

  • __next__() -- pada iterator, mengembalikan item berikutnya atau memunculkan StopIteration ketika tidak ada lagi item.

Built-in iter() memanggil __iter__; next() memanggil __next__. Telusuri daftar secara manual:

it = iter([10, 20, 30])
print(next(it))    # 10
print(next(it))    # 20
print(next(it))    # 30
print(next(it))    # raises StopIteration
A for loop calls __iter__ once to get an iterator, then calls __next__ repeatedly until StopIteration ends the loop.

for adalah singkatan dari "panggil __iter__ sekali, lalu loop pada __next__ hingga StopIteration."

Yang sebenarnya dilakukan oleh for x in items::

_it = iter(items)
while True:
    try:
        x = next(_it)
    except StopIteration:
        break
    # ... loop body ...

Setiap list, tuple, string, dict, set, objek file, dan generator sudah mengimplementasikan __iter__ dan __next__ -- itulah mengapa semuanya bekerja dengan for.

2.31.2. yield dan fungsi generator

Fungsi yang mengandung pernyataan yield adalah fungsi generator. Memanggilnya tidak menjalankan badan fungsinya; melainkan mengembalikan objek generator (sebuah iterator) yang menjalankan badan fungsi satu yield pada satu waktu:

def count_up_to(n):
    i = 0
    while i < n:
        yield i
        i += 1

for value in count_up_to(3):
    print(value)

Output:

0
1
2

Setiap panggilan ke next() melanjutkan fungsi hingga yield berikutnya, menyerahkan nilai tersebut ke pemanggil, dan berhenti di sana. Status lokal (i dalam kasus ini) dipertahankan antar pelanjutan.

Sequence diagram with two lifelines (caller and generator body). The caller calls count_up_to(3), which creates a generator without running the body. Each subsequent next() runs the body until the next yield, returns the yielded value, and pauses. The fourth next() falls off the end and raises StopIteration. The variable i is preserved across pauses.

next() menjalankan badan hingga yield berikutnya, menyerahkan nilai kembali, dan berhenti. Status lokal bertahan saat jeda.

Generator adalah cara termudah untuk menghasilkan urutan secara malas -- tidak ada list yang dibangun, item hanya dihitung ketika konsumen memintanya, dan fungsi dapat menghasilkan item selamanya jika diinginkan.

2.31.3. Pipeline malas

Generator dapat dikombinasikan dengan baik. Output satu generator dapat menjadi input generator lainnya:

def numbers():
    i = 0
    while True:
        yield i
        i += 1

def squares(source):
    for x in source:
        yield x * x

pipeline = squares(numbers())

for v in pipeline:
    if v > 100:
        break
    print(v)

Nilai mengalir melalui pipeline satu per satu -- tidak ada list perantara, tidak ada batas atas yang ditentukan dalam numbers, dan konsumen (for v in pipeline) memutuskan kapan harus berhenti.

Three boxes left-to-right: numbers(), squares(source), and the for-v-in-pipeline consumer. Three cycles are drawn beneath. In each cycle, the consumer sends a pull request leftward to squares, which sends a pull leftward to numbers; numbers yields a value rightward to squares, which yields its squared value rightward to the consumer.

Setiap next() pada konsumen memicu satu pull melalui rantai; nilai hanya ada ketika sesuatu memintanya.

2.31.3.1. yield from

Loop yang menarik item dari iterable lain dan menghasilkan masing-masing item cukup umum sehingga Python menyediakan pintasan. Ekspresi yield from iter menghasilkan setiap nilai yang dihasilkan iterable tersebut, secara berurutan -- seolah-olah generator memiliki loop for x in iter: yield x secara langsung:

def chain(*sources):
    for source in sources:
        yield from source

for v in chain([1, 2, 3], (4, 5), "abc"):
    print(v)

Output:

1
2
3
4
5
a
b
c

yield from persis setara dengan loop for eksplisit, hanya lebih pendek, dan ia menyebarkan StopIteration dari iterable dalam ke generator luar dengan bersih -- berguna ketika menggabungkan beberapa generator secara berurutan.

2.31.3.2. Ketika yield habis

Melewati akhir fungsi generator (atau menekan return eksplisit) secara otomatis memunculkan StopIteration. Tidak perlu memunculkannya secara manual; loop for yang mengitasinya akan melihatnya dan berhenti.

Gunakan generator ketika kode penghasil secara alami ditulis sebagai loop dengan beberapa titik yield; gunakan list comprehension biasa ketika Anda benar-benar membutuhkan seluruh urutan dalam memori.