טיפ פייתון: גנרטורים ו 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 מחבר את כולם יחד.