Kodokon kodokon.com

อิเทอเรเตอร์และเจเนอเรเตอร์: yield และไปป์ไลน์

เชี่ยวชาญโปรโตคอลการวนซ้ำและเจเนอเรเตอร์ เพื่อประมวลผลสตรีมข้อมูลแบบขี้เกียจ ด้วยหน่วยความจำคงที่

10 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

for ของ Python ไม่ใช่อะไรมากไปกว่าโปรโตคอลหนึ่ง: iter(obj) ขอ อิเทอเรเตอร์ จากนั้น next() จะถูกเรียกไปเรื่อย ๆ จนเกิดข้อยกเว้น StopIteration จงแยกให้ออกระหว่าง อ็อบเจกต์ที่วนซ้ำได้ (iterable) (เช่นลิสต์ ที่มอบอิเทอเรเตอร์ตัวใหม่ให้ได้ในทุกรอบ) กับ อิเทอเรเตอร์ (ตัวชี้ตำแหน่ง ที่เดินผ่านได้เพียงครั้งเดียว) การเข้าใจโปรโตคอลนี้คือการเข้าใจว่าทำไมอ็อบเจกต์บางตัวจึง "ถูกใช้จนหมด"

PYTHON
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))
# next(iterator) would raise StopIteration
สิ่งที่ลูป for ทำ ในแบบทำมือ

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

PYTHON
def squares(limit):
    n = 0
    while n < limit:
        yield n * n
        n += 1

gen = squares(4)
print(next(gen))  # 0
print(list(gen))  # [1, 4, 9]
yield หยุดพักฟังก์ชันไว้ระหว่างค่าสองค่า

ประโยชน์หลักในงานจริงคือ ไปป์ไลน์ แต่ละขั้นบริโภคสตรีมจากขั้นก่อนหน้าและผลิตสตรีมของตัวเองออกมาแบบขี้เกียจ ไม่มีอะไรทำงานเลยก่อนการบริโภคขั้นสุดท้าย (list(), sum() หรือลูป for) ข้อแลกเปลี่ยนที่ต้องจำไว้: ลิสต์เดินผ่านได้หลายครั้งและตัดแบ่ง (slice) ได้ ส่วนเจเนอเรเตอร์ทำไม่ได้ แต่ในทางกลับกัน เจเนอเรเตอร์ประมวลผลไฟล์ขนาด 10 GB ได้โดยไม่ทำให้ RAM เต็ม นิพจน์เจเนอเรเตอร์ - (f(x) for x in xs if cond) - ครอบคลุมขั้นตอนง่าย ๆ ได้โดยไม่ต้องนิยามฟังก์ชัน

PYTHON
lines = ["  12 ", "x", " 7", "", "30"]

def clean(rows):
    for row in rows:
        row = row.strip()
        if row.isdigit():
            yield int(row)

def keep_from(values, floor):
    return (v for v in values if v >= floor)

pipeline = keep_from(clean(lines), 10)
print(list(pipeline))  # [12, 30]
ไปป์ไลน์แบบขี้เกียจ: ไม่มีอะไรทำงานก่อน list()

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. คุณจะได้อะไรเมื่อเรียกฟังก์ชันที่ตัวโค้ดของมันมี yield อยู่?
    • ผลลัพธ์ของ yield ตัวแรก
    • เจเนอเรเตอร์ โดยตัวโค้ดยังไม่ทำงานเลย
    • ลิสต์ของค่าทั้งหมดที่ถูกผลิตออกมา
  2. ไปป์ไลน์ของเจเนอเรเตอร์มีข้อได้เปรียบชี้ขาดอะไรเหนือลิสต์ตัวกลาง?
    • ผลลัพธ์ถูกเรียงลำดับให้อัตโนมัติ
    • หน่วยความจำคงที่: ค่าถูกผลิตขึ้นตามที่ร้องขอ
    • การประมวลผลถูกกระจายขนานไปหลายคอร์
  3. หลังจาก list(gen) ครั้งแรก การเรียก list(gen) ครั้งที่สองบนเจเนอเรเตอร์ตัวเดิมจะคืนอะไร?
    • ลิสต์เดียวกับการเรียกครั้งแรก
    • ลิสต์ว่าง
    • ข้อยกเว้นถูกส่งต่อออกมา