תרגיל פייתון: הרחבת pairwise עם גודל וגודל צעד
הפונקציה itertools.pairwise בפייתון לוקחת רשימה ומחזירה רשימה של זוגות חופפים מתוך הקלט המקורי, לדוגמה:
>>> import itertools
>>> 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)]
בואו נרחיב אותה באמצעות שני פרמטרים:
פרמטר size קובע את גודל ה״זוג״ (וכן כל מספר גדול מ-2 כבר לא יהיה זוג).
פרמטר stride קובע את גודל הצעד כלומר החפיפה בין הזוגות.
לדוגמה ארצה להיות מסוגל להפעיל את הפונקציה כדי לקבל את כל השלשות החופפות בטווח המספרים עד 100, או לקבל קבוצות של 5 מספרים בהפרש של 3 אחת מהשנייה.
1. פיתרון
כדי לפתור את התרגיל אני מתחיל עם הקוד של pairwise עצמה מתוך דף התיעוד:
def pairwise(iterable):
# pairwise('ABCDEFG') → AB BC CD DE EF FG
iterator = iter(iterable)
a = next(iterator, None)
for b in iterator:
yield a, b
a = b
בעצם הפונקציה מבצעת איטרציה כפולה - לוקחים את האיבר הראשון למשתנה a, ואז מתחילים לרוץ על האיטרטור עם המשתנה b שעכשיו מקבל את האיבר השני (כי הראשון כבר נלקח ל a). מחזירים את הזוג וממשיכים לסיבוב נוסף, הפעם a מקבל את הערך של b כלומר את האיבר השני ו b מקבל את הדבר הבא מהאיטרטור שזה כבר האיבר השלישי.
ומכאן ההרחבה למקרה הכללי ברורה - במקום לקחת רק איבר אחד קדימה עם next נרצה לקחת בלוק שלם קדימה עם itertools.islice. זה הקוד:
import itertools
def each_cons(iterable, size=2, stride=1):
iterator = iter(iterable)
chunk = tuple(itertools.islice(iterator, size))
for b in iterator:
yield chunk
chunk = chunk[stride:] + (b,) + tuple(itertools.islice(iterator, stride - 1))
yield chunk
נשים לב:
בגלל העבודה עם איטרטורים אין איטרציה כפולה, כלומר הקוד לא צריך לטעון את כל המידע לזיכרון לפני שמייצר את הזוגות (או שלשות, או רביעיות).
הכתיב
(b,)זאת הדרך של פייתון לייצר tuple של איבר אחד.
הקוד הזה עוזר להמחיש את הפיתרון אבל הוא לא מושלם. קל לראות שכש stride גדול מ size מקבלים chunk-ים גדולים מדי (בגלל אופן היצירה של chunk), אין וולידציה של פרמטרים וכל חיבורי ה tuples האלה יכולים לפגוע בביצועים. נתתי לג'מיני לשפר את המימוש וקיבלתי את הקוד הזה:
import itertools
from collections import deque
def each_cons(iterable, size=2, stride=1):
"""
Yields overlapping tuples of a given size and stride.
Like a sliding window, but the window can jump.
Args:
iterable: The iterable to process.
size: The size of each chunk (window).
stride: The number of items to advance between chunks.
Yields:
A tuple of length `size` for each step.
Examples:
>>> list(each_cons([1, 2, 3, 4, 5], size=3, stride=1))
[(1, 2, 3), (2, 3, 4), (3, 4, 5)]
>>> list(each_cons([1, 2, 3, 4, 5, 6], size=3, stride=2))
[(1, 2, 3), (3, 4, 5)]
>>> list(each_cons([1, 2], size=3))
[]
"""
if size < 1 or stride < 1:
raise ValueError("size and stride must be positive integers")
# Create 'size' independent iterators
iterators = itertools.tee(iterable, size)
# Advance each iterator to its starting position
for i, it in enumerate(iterators):
# The `deque` trick is a fast way to advance an iterator
# by 'i' steps without storing the results.
deque(itertools.islice(it, i), maxlen=0)
# Zip the advanced iterators together to create the sliding window
# of size `size` and stride `1`.
windowed_iterator = zip(*iterators)
# If stride is 1, we're done. Otherwise, apply the stride
# by taking every Nth item from the windowed iterator.
if stride == 1:
return windowed_iterator
else:
return itertools.islice(windowed_iterator, None, None, stride)
הקוד של ג'מיני מטפל טוב יותר במקרי הקצה ואולי גם יותר יעיל. החיסרון שלו הוא שהוא מושך פריטים מה iterable גם לפני שהקוד החיצוני ביקש אותם. אגב כששאלתי אותו על זה ג'מיני מיהר להתגונן ולהסביר שב 99% מהמקרים זו לא בעיה אני לא בטוח שאני מסכים איתו.
יש לכם רעיון נוסף איך לממש חלון גולש בפייתון ורוצים לשתף? אפשר ורצוי להדביק בתגובות או בטלגרם.