לדלג לתוכן

סקופ משתנים בפייתון מוסבר - למה המשתנה שלכם "נעלם"

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

מה זה בכלל סקופ

סקופ הוא פשוט התשובה לשאלה "איפה משתנה מסוים נגיש". לא כל משתנה קיים בכל מקום בתוכנית - משתנה שנוצר בתוך פונקציה קיים רק בתוכה, ונעלם ברגע שהפונקציה מסתיימת:

def greet():
    message = "שלום"
    print(message)

greet()
print(message)  # שגיאה - message לא קיים כאן

השורה האחרונה תזרוק NameError, כי message הוא משתנה מקומי - local - שקיים רק בתוך greet, ופייתון פשוט לא מכיר אותו מחוץ לה.

כלל ה-LEGB

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

  • Local - המשתנים המקומיים של הפונקציה הנוכחית.
  • Enclosing - המשתנים של פונקציה חיצונית, אם הפונקציה הנוכחית מקוננת בתוכה.
  • Global - המשתנים שהוגדרו ברמת הקובץ, מחוץ לכל פונקציה.
  • Built-in - שמות מובנים בפייתון עצמה, כמו len או print.

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

x = "גלובלי"

def outer():
    x = "חיצוני"

    def inner():
        print(x)  # מוצא את x מה-enclosing scope

    inner()

outer()

המילה global ולמה כדאי להיזהר ממנה

עכשיו נחזור לבעיה שפתחנו איתה. למה שינוי משתנה בתוך פונקציה לא משפיע על החוץ?

counter = 0

def increment():
    counter = counter + 1  # שגיאה!

increment()

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

כדי לגרום לפונקציה לשנות משתנה גלובלי בפועל, צריך להצהיר על כך במפורש עם global:

counter = 0

def increment():
    global counter
    counter = counter + 1

increment()
print(counter)  # 1

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

def increment(counter):
    return counter + 1

counter = increment(counter)

nonlocal בפונקציות מקוננות

יש מקרה דומה לגמרי, אבל עם פונקציה מקוננת שרוצה לשנות משתנה של הפונקציה החיצונית שלה, לא של הקובץ כולו. שם global לא עוזר, כי הגלובלי הוא לא איפה שהמשתנה נמצא. לזה יש מילת מפתח נפרדת - nonlocal:

def make_counter():
    count = 0

    def increment():
        nonlocal count
        count += 1
        return count

    return increment

counter = make_counter()
print(counter())  # 1
print(counter())  # 2

זה תבנית שנקראת closure - הפונקציה הפנימית "זוכרת" את הסביבה שבה היא נוצרה, ו-nonlocal מאפשר לה גם לשנות אותה, לא רק לקרוא ממנה. שימו לב שגם כאן, בלי nonlocal, שורת count += 1 הייתה יוצרת משתנה מקומי חדש בשם count בתוך increment, ותקבלו בדיוק אותה שגיאת UnboundLocalError שראינו קודם עם global.

למה זה חשוב מעבר לפונקציה בודדת

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

לסיכום

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

הצטרפו לקהילה בדיסקורד