גנרטורים ב-Python

גנרטורים ב-Python

מאמר זה מסביר את מנגנון הגנרטורים ב-Python.

YouTube Video

גנרטורים ב-Python

סקירה כללית

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

מהו גנרטור?

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

יסודות מילת המפתח yield

yield היא מילת מפתח שמחזירה ערך וגם משהה את ביצוע הפונקציה באותו רגע.

 1def simple_generator():
 2    yield 1
 3    yield 2
 4    yield 3
 5
 6gen = simple_generator()
 7
 8print(next(gen))  # 1
 9print(next(gen))  # 2
10print(next(gen))  # 3
  • כאשר קוראים לה, פונקציה זו מחזירה אובייקט גנרטור שמניב ערכים אחד אחד.
  • אם תקרא ל-next() כאשר אין ערך הבא, תתרחש שגיאת StopIteration.

next() ו-StopIteration

 1def simple_generator():
 2    yield 1
 3    yield 2
 4    yield 3
 5
 6gen = simple_generator()
 7
 8try:
 9    while True:
10        value = next(gen)
11        print(value)
12except StopIteration:
13    print("Finished")
  • על־ידי טיפול מפורש בשגיאת StopIteration כך, אפשר לזהות מתי גנרטור סיים לפעול.

send(value)

קריאה ל־send(value) מחדשת את הגנרטור ושולחת את הערך value למיקום של הביטוי yield. הערך שנשלח ניתן להתקבל בצד של הגנרטור כ־ערך ההחזר של ביטוי ה־yield. בקריאה הראשונה אי אפשר לשלוח שום דבר חוץ מ־None עם send(value), לכן יש להשתמש ב־next() או ב־send(None).

 1def gen():
 2    x = yield 1
 3    print(f"x = {x}")
 4    y = yield 2
 5    print(f"y = {y}")
 6
 7g = gen()
 8print(next(g))       # -> 1 (value from yield 1)
 9print(g.send(10))    # -> x = 10, 2 (value from yield 2)
10print(g.send(20))    # -> y = 20, StopIteration occurs
  • עם send(10), ה־yield של הגנרטור נהפך ל־ביטוי שמחזיר 10, ו־10 מוקצה ל־x.

throw()

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

 1def gen():
 2    try:
 3        yield 1
 4    except ValueError as e:
 5        print(f"Caught: {e}")
 6        yield "recovered"
 7
 8g = gen()
 9print(next(g))   # -> 1
10print(g.throw(ValueError("boom")))  # -> Caught: boom, "recovered"
  • בקוד הזה, קריאה ל־throw מזריקה חריג לתוך הגנרטור. בצד של הגנרטור החריג נתפס ומוחזר הערך recovered.

close()

קריאה ל־close() מסיימת את הגנרטור. בתוך הגנרטור אפשר לבצע ניקוי בעזרת finally. קריאה ל־next() או ל־send() אחרי קריאה ל־close() תגרום לשגיאת StopIteration.

1def gen():
2    try:
3        yield 1
4    finally:
5        print("Cleaning up...")
6
7g = gen()
8print(next(g))  # -> 1
9g.close()       # -> Cleaning up...
  • הקוד הזה מדגים שקריאה ל־close() מסיימת את הגנרטור ומפעילה את תהליך הניקוי שנמצא ב־finally.

yield from

yield from‎ היא תחביר המשמשת להאצלת ביצוע לתת-גנרטור. זוהי דרך פשוטה לקרוא לגנרטור נוסף מתוך גנרטור ו־להעביר את כל הערכים שלו החוצה.

1def sub_gen():
2    yield 1
3    yield 2
4
5def main_gen():
6    yield from sub_gen()
7    yield 3
8
9print(list(main_gen()))  # -> [1, 2, 3]
  • הקוד הזה מאציל את כל הערכים מהתת־גנרטור לגנרטור החיצוני בעזרת ‎yield from‎ ואז מחזיר את ‎3‎.

הקשר בין גנרטורים לאיטרטורים

גנרטורים מממשים באופן פנימי את __iter__() ו-__next__(), ולכן הם סוג של איטרטור. לכן, הם תואמים לחלוטין לפעולות איטרטיביות כמו לולאות for.

שילוב עם לולאות for

ב-Python, לולאת for משתמשת ב-next() באופן פנימי כדי לאסוף ערכים באופן אוטומטי.

1def simple_generator():
2    yield 1
3    yield 2
4    yield 3
5
6for value in simple_generator():
7    print(value)

באופן זה, גם הטיפול ב-StopIteration נעשה אוטומטית.

יצירת גנרטורים אינסופיים

1def count_up(start=0):
2    while True:
3        yield start
4        start += 1
5
6counter = count_up()
7print(next(counter))  # 0
8print(next(counter))  # 1

ניתן ליצור לולאות אינסופיות, אך יש להשתמש בהן בזהירות.

ביטויי גנרטורים

ביטויי גנרטורים הנכתבים בסוגריים מעוגלים, מאפשרים להגדיר גנרטור בסינטקס הדומה ל-list comprehensions.

1# List comprehension (generates the entire list at once)
2squares_list = [x**2 for x in range(5)]
3print(squares_list)
4
5# Generator expression
6squares_gen = (x**2 for x in range(5))
7for square in squares_gen:
8    print(square)

בניגוד ל-list comprehensions, הם לא טוענים את כל האיברים לזיכרון בבת אחת, ולכן חסכוניים יותר בזיכרון.

טיפול בשגיאות בגנרטורים

חריגות עשויות להתרחש בתוך גנרטור. במקרים כאלה, יש להשתמש ב-try-except כמו בקוד Python רגיל.

 1def safe_divide_generator(numbers, divisor):
 2    """Yields results of dividing numbers by a given divisor safely."""
 3    for number in numbers:
 4        try:
 5            yield number / divisor  # Attempt to divide and yield result.
 6        except ZeroDivisionError:
 7            yield float('inf')  # Return infinity if division by zero occurs.
 8
 9# Example usage
10numbers = [10, 20, 30]
11gen = safe_divide_generator(numbers, 0)  # Create generator with divisor as 0.
12for value in gen:
13    print(value)  # Output: inf, inf, inf

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

מעקב מחסנית של גנרטור

אם מתרחשת חריגה בתוך הגנרטור, היא תועלה כאשר הגנרטור ימשיך לפעול.

 1def error_generator():
 2    """A generator that yields values and raises an error."""
 3    yield 1
 4    raise ValueError("An error occurred")  # Raise a ValueError intentionally.
 5    yield 2
 6
 7gen = error_generator()
 8print(next(gen))       # Output: 1 (first value yielded)
 9try:
10    print(next(gen))   # Attempt to get the next value, which raises an error
11except ValueError as e:
12    print(e)           # Output: An error occurred (exception message is printed)
  • הגנרטור הזה מחזיר קודם כל את 1. השגיאה שמתרחשת כאשר ממשיכים את הגנרטור נתפסת ומוצגת כהודעת שגיאה.

דוגמאות לשימוש בגנרטורים

קריאת קובץ שורה אחר שורה (מתאים לקבצים גדולים)

1def read_large_file(filepath):
2    with open(filepath, 'r') as f:
3        for line in f:
4            yield line.strip()
  • הפונקציה הזו קוראת קובץ טקסט שורה אחר שורה בעזרת איטרטור, מסירה רווחים מיותרים מכל שורה, ומחזירה את זה כגנרטור, ובכך מאפשרת לעבד קבצים גדולים בצריכת זיכרון נמוכה.

גנרטור לסדרת פיבונאצ'י

1def fibonacci(limit):
2    a, b = 0, 1
3    while a < limit:
4        yield a
5        a, b = b, a + b
6
7for n in fibonacci(100):
8    print(n)
  • הקוד הזה עושה שימוש בגנרטור כדי ליצור מספרי פיבונאצ׳י בסדר עד גבול עליון, ומדפיס אותם באמצעות לולאת for.

מקרי שימוש

גנרטורים יכולים לשמש גם בתרחישים הבאים.

  • עיבוד סדרתי של קבצי CSV או קבצי לוג גדולים
  • דפדוף ב-API
  • עיבוד נתוני סטרימינג (כגון Kafka, מכשירי IoT)

סיכום

מושג עיקרון מרכזי
yield משהה ומחזירה ערך
פונקציית גנרטור פונקציה הכוללת yield ומחזירה איטרטור בעת הקריאה
יתרונות חסכון בזיכרון ומתאים לעיבוד נתונים גדולים
ביטוי גנרטור מאפשר סינטקס תמציתי כגון (x for x in iterable)

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

תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.

YouTube Video