2.31. Iterators และ generators

for loop ทำงานหนักกว่าที่เห็น หน้านี้ครอบคลุม iterator protocol ที่มันทำงานอยู่ และคีย์เวิร์ด yield ที่ให้คุณสร้าง iterator ของตัวเอง

2.31.1. Iterator protocol

ทุกออบเจกต์ที่สามารถวนซ้ำได้ต้องใช้งานสองเมธอด:

  • __iter__() -- คืนค่า iterator ของรายการในออบเจกต์

  • __next__() -- บน iterator, คืนค่ารายการถัดไปหรือ raise StopIteration เมื่อไม่มีรายการเหลือแล้ว

built-in iter() เรียก __iter__; next() เรียก __next__ ลองทำงานผ่านรายการด้วยตนเอง:

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 คือ syntax sugar ของ "เรียก __iter__ ครั้งเดียว จากนั้นวนซ้ำบน __next__ จนถึง StopIteration"

สิ่งที่ for x in items: ทำจริงๆ:

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

ทุก list, tuple, string, dict, set, file object และเจนเนอเรเตอร์ใช้งาน __iter__ และ __next__ อยู่แล้ว ดังนั้นจึงทำงานกับ for ได้ทั้งหมด

2.31.2. ฟังก์ชัน yield และ generator

ฟังก์ชันที่มีคำสั่ง yield คือ generator function การเรียกใช้มันจะไม่รันตัวฟังก์ชัน แต่คืนค่า generator object (ซึ่งเป็น iterator) ที่รันตัวฟังก์ชันทีละ yield:

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

การเรียก next() แต่ละครั้งจะกลับมาทำงานต่อในฟังก์ชันจนถึง yield ถัดไป ส่งค่านั้นให้ผู้เรียก และหยุดชั่วคราวตรงนั้น สถานะโลคอล (ในกรณีนี้คือ i) จะถูกเก็บรักษาระหว่างการกลับมาทำงาน

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() รันตัวฟังก์ชันจนถึง yield ถัดไป ส่งค่ากลับ และหยุดชั่วคราว สถานะโลคอลจะรอดพ้นจากการหยุดชั่วคราว

เจนเนอเรเตอร์คือวิธีที่ง่ายที่สุดในการสร้างลำดับแบบ lazy ไม่มีการสร้าง list ขึ้นมา รายการจะถูกคำนวณเฉพาะเมื่อผู้ใช้งานต้องการ และฟังก์ชันสามารถ yield รายการไปเรื่อยๆ ได้ตลอดไปหากต้องการ

2.31.3. Pipeline แบบ lazy

เจนเนอเรเตอร์ประกอบกันได้ดี output ของเจนเนอเรเตอร์หนึ่งสามารถป้อนให้กับอีกอันได้:

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)

ค่าจะไหลผ่าน pipeline ทีละค่า ไม่มี list กลางๆ ไม่มีขอบเขตบนที่ฝังอยู่ใน numbers และผู้ใช้งาน (for v in pipeline) เป็นผู้ตัดสินใจเมื่อต้องการหยุด

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.

next() แต่ละครั้งบน consumer จะทริกเกอร์การดึงหนึ่งครั้งตลอดเชน ค่าจะมีอยู่เฉพาะเมื่อมีบางอย่างถามหา

2.31.3.1. yield from

การวนซ้ำที่ดึงรายการจาก iterable อื่นและ yield แต่ละรายการเป็นเรื่องที่พบบ่อยพอที่ Python จัดเตรียม shortcut ไว้ นิพจน์ yield from iter จะ yield ค่าแต่ละค่าที่ iterable สร้าง ตามลำดับ เสมือนว่าเจนเนอเรเตอร์มีลูป for x in iter: yield x อยู่ inline:

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 เทียบเท่ากับลูป for ที่เขียนชัดเจนทุกประการ แต่สั้นกว่า และมันจัดการ StopIteration จาก iterable ภายในขึ้นไปยังเจนเนอเรเตอร์ภายนอกอย่างสะอาด ซึ่งมีประโยชน์เมื่อต่อเจนเนอเรเตอร์หลายๆ อันเรียงกัน

2.31.3.2. เมื่อ yield หมด

การตกออกจากปลาย generator function (หรือพบ return โดยชัดเจน) จะ raise StopIteration โดยอัตโนมัติ ไม่จำเป็นต้อง raise มันด้วยมือ ลูป for ที่ล้อมรอบจะเห็นมันและสิ้นสุดลง

ใช้เจนเนอเรเตอร์เมื่อโค้ดที่สร้างข้อมูลเขียนได้เป็นธรรมชาติเป็นลูปที่มีจุด yield บ้าง ใช้ list comprehension ธรรมดาเมื่อคุณต้องการลำดับทั้งหมดในหน่วยความจำจริงๆ