• בלוג
  • טיפ פייתון: גנרטורים ו StopIteration

טיפ פייתון: גנרטורים ו StopIteration

במודול itertools של פייתון יש פונקציה בשם pairwise שמקבלת איטרטור ומחלקת אותו לזוגות. זה נראה ככה:

>>> list(itertools.pairwise(range(100)))
[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 12), (12, 13), (13, 14), (14, 15), (15, 16), (16, 17), (17, 18), (18, 19), (19, 20), (20, 21), (21, 22), (22, 23), (23, 24), (24, 25), (25, 26), (26, 27), (27, 28), (28, 29), (29, 30), (30, 31), (31, 32), (32, 33), (33, 34), (34, 35), (35, 36), (36, 37), (37, 38), (38, 39), (39, 40), (40, 41), (41, 42), (42, 43), (43, 44), (44, 45), (45, 46), (46, 47), (47, 48), (48, 49), (49, 50), (50, 51), (51, 52), (52, 53), (53, 54), (54, 55), (55, 56), (56, 57), (57, 58), (58, 59), (59, 60), (60, 61), (61, 62), (62, 63), (63, 64), (64, 65), (65, 66), (66, 67), (67, 68), (68, 69), (69, 70), (70, 71), (71, 72), (72, 73), (73, 74), (74, 75), (75, 76), (76, 77), (77, 78), (78, 79), (79, 80), (80, 81), (81, 82), (82, 83), (83, 84), (84, 85), (85, 86), (86, 87), (87, 88), (88, 89), (89, 90), (90, 91), (91, 92), (92, 93), (93, 94), (94, 95), (95, 96), (96, 97), (97, 98), (98, 99)]

בשביל לחלק לקבוצות יותר גדולות אין פיתרון מובנה, אבל אנחנו יכולים לבנות אחד יחסית בקלות. בואו ננסה ניסיון ראשון:

def nwise(iterable, n):
    while True:
        yield [next(iterable) for i in range(n)]

אבל כשאני מנסה להפעיל את הפונקציה באותה צורה כמו pairwise אני נכשל:

>>> list(nwise(range(100), 2))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in nwise
  File "<stdin>", line 3, in <listcomp>
TypeError: 'range' object is not an iterator

וזה ברור - range אינו איטרטור. הפונקציות המובנות של itertools יודעות לעבוד עם iterable, כלומר משהו שאפשר לבנות ממנו איטרטור. הטעות הראשונה היא שלא יצרתי איטרטור מה iterable שהעבירו לי. תיקון ראשון ועוד ניסיון הרצה:

def nwise(iterable, n):
    it = iter(iterable)
    while True:
        yield [next(it) for i in range(n)]


>>> list(nwise(range(100), 2))
Traceback (most recent call last):
  File "<stdin>", line 4, in nwise
  File "<stdin>", line 4, in <listcomp>
StopIteration

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: generator raised StopIteration

הפעם הודעת שגיאה חדשה - בתוך לולאת ה while של nwise קראתי ל next עם פרמטר אחד. בהנחה שאין כלום באיטרטור, הקריאה תזרוק Exception וזה מה שאנחנו רואים בפלט.

תיקון אחד קל כאן הוא לתפוס את ה Exception בתוך הגנרטור ופשוט לסיים את הלולאה:

def nwise(iterable, n):
    it = iter(iterable)
    while True:
        try:
            yield [next(it) for i in range(n)]
        except StopIteration:
            break

תיקון שני קצת יותר מתוחכם אבל שיכול להפוך את הקוד הזה להרבה יותר פשוט הוא לוותר על הקריאה ל next ולהשתמש ב itertools כל הדרך:

def nwise(iterable, n):
    return zip(*[itertools.islice(iterable, i, None, n) for i in range(n)])

הרעיון הוא לקחת את ה iterable שלנו, לייצר ממנו n רצפים כשכל אחד מהם מתחיל ערך-אחד-קדימה ומתקדם בכפולות של n, ואז zip מחבר את כולם יחד.