• בלוג
  • מי בכלל צריך Generators

מי בכלל צריך Generators

03/04/2017

פוסט זה כולל טיפ קצר לעבודה יעילה עם Python. אם אתם רוצים ללמוד פייתון יותר לעומק אני ממליץ על קורס Python כאן באתר.
הקורס כולל עשרות שיעורי וידאו והמון תרגול מעשי וילמד אתכם Python בצורה מקצועית מההתחלה ועד הנושאים המתקדמים.

תחביר הלולאות של Python כולל קסם קטן ומובלע שלא מוצאים בהרבה שפות, והוא השימוש ב Iterators. נראה למה זה טוב דרך מספר דוגמאות.

1. תחביר

עיקר הקסם התחבירי בכלל לא קורה באיטרטורים אלא בלולאת for של פייתון. אחת הדוגמאות הראשונות שנתקלים בהם כשלומדים פייתון היא השילוב של for עם range:

for i in range(10):
    print(i)

המבנה דומה ללולאות foreach משפות אחרות, ולי הזכיר את csh ו perl. אבל זהו דמיון שטחי בלבד. הפונקציה range כשמופעלת לבדה מחזירה בכלל אוביקט. על אוביקט זה יש להפעיל את הפונקציה iter כדי לקבל איטרטור (שזהו אוביקט נוסף שתפקידו לסרוק את האוביקט המקורי), ואיטרטור בתורו הוא גם אוביקט שהפעלת הפונקציה next עליו מחזירה את האיבר הבא. כלומר בשביל לממש את לולאת for באמצעות לולאת while נצטרך משהו בסגנון הזה:

def myfor(f, seq):
    try:
        i = iter(seq)

        while True:
            f(next(i))

    except StopIteration as s:
        pass

כל לולאת for בפייתון עובדת כך מתחת לפני השטח: היא מפעילה את iter כדי לייצר אוביקט סריקה מתוך הרשימה, ולאחר מכן מפעילה next על אוביקט הסריקה עד שנזרקת הודעת StopIteration שגורמת ליציאה מהלולאה.

ועכשיו ל Generators: מבנה זה הוא קיצור דרך ליצירת רצפים שאפשר לרשום בתוך for. כך למשל אפשר היה לממש גירסא ממש פשוטה של range:

def myrange(n):
    i = 0
    while i < n:
        yield i
        i += 1

for n in myrange(10):
    print(n)

כל קריאה ל next גורמת לריצה של הפונקציה עד שמגיעים ל yield, מחזירה את הערך ש yield קיבל ובפעם הבאה שתקרא next על אותו אוביקט היא תדע להמשיך מאותה הנקודה. רק בשביל המשחק נפעיל את הפונקציה שלנו מחוץ ללולאת for:

>>> myrange(10)
<generator object myrange at 0x10a83a308>
>>> myrange(10)
<generator object myrange at 0x10a83a360>

>>> g = myrange(10)
>>> next(g)
0
>>> next(g)
1

כל הפעלה של הפונקציה מחזירה אוביקט Generator חדש, וכל אוביקט Generator זוכר איפה עצר כך שבקריאה ל next עליו הפונקציה ממשיכה מאותה הנקודה.

קיצור דרך נוסף של פייתון הוא ה Generator Expression, שזה ביטוי שנראה כמו List Comprehension אבל בעצם לא מחשב את כל הרשימה אלא רק מחזיר Generator לקבלת האיבר הבא. דוגמא פשוטה לעניין היא:

(x*x for x in range(10))

2. גנרטורים חוסכים זכרון

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

# Version 1 - with generators, using less memory
sum(x*x for x in range(10000))

# Version 2 - without generators, saving entire list in memory
sum([x*x for x in range(10000)])

הגירסא השניה מחשבת את כל רשימת המספרים ולאחר מכן סוכמת אותה, ולכן תשמור בזיכרון 10,000 מספרים. הראשונה לעומתה מוסיפה כל פעם את המספר הבא וצריכה לשמור בזיכרון רק ערך אחד, את תוצאת החישוב.

3. גנרטורים מאפשרים פיצול של הקוד

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

import sys

with open('input.txt', 'r') as f:
    for line in f:
        if line[0].isupper()
            sys.stdout.write(line)

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

import sys

def each_line(fname):
    with open(fname, 'r') as f:
        for line in f:
            yield line

def ucfirst(line):
    return line[0].isupper()

for line in filter(ucfirst, each_line('input.txt')):
    sys.stdout.write(line)

וזה כבר ממש נחמד כי אפשר להוסיף עוד ועוד פונקציות לקבל פונקציונאליות מורכבת יותר:

import sys

def each_line(fname):
    with open(fname, 'r') as f:
        for line in f:
            yield line

def ucfirst(line):
    return line[0].isupper()

def long(line):
    return len(line) > 50 

for line in filter(long, filter(ucfirst, each_line('input.txt'))):
    sys.stdout.write(line)

לפייתון עצמה יש יכולות הרכבת Generators מאוד מתוחכמות. הפונקציה filter מחזירה איטרטור וכמוה גם map, ורבות נוספות נמצאות במודול itertools.

שימוש ב Generators הוא דרך פשוטה לפצל קוד ולחסוך בזיכרון. המבנה של לולאת for והתמיכה המובנית ב Iterators הם שנותנים ל Generatos את עיקר הכח בשפה.