2.31. Iterators และ generators#
for loop ทำงานหนักกว่าที่เห็น หน้านี้ครอบคลุม iterator protocol ที่มันทำงานอยู่ และคีย์เวิร์ด yield ที่ให้คุณสร้าง iterator ของตัวเอง
2.31.1. Iterator protocol#
ทุกออบเจกต์ที่สามารถวนซ้ำได้ต้องใช้งานสองเมธอด:
__iter__()-- คืนค่า iterator ของรายการในออบเจกต์__next__()-- บน iterator, คืนค่ารายการถัดไปหรือ raiseStopIterationเมื่อไม่มีรายการเหลือแล้ว
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
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) จะถูกเก็บรักษาระหว่างการกลับมาทำงาน
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) เป็นผู้ตัดสินใจเมื่อต้องการหยุด
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 ธรรมดาเมื่อคุณต้องการลำดับทั้งหมดในหน่วยความจำจริงๆ