Scope — Local, Enclosing, Global ו-Built-in (LEGB)

מטרות השיעור
  • להסביר מה זה Scope ולמה משתנה שנוצר בפונקציה לא קיים מחוצה לה
  • להכיר את ארבעת ה-Scopes — Local, Enclosing, Global ו-Built-in — ואת סדר החיפוש LEGB
  • להבין מה קורה כשמשנים משתנה גלובלי בתוך פונקציה, ומה עושה global
  • לכתוב פונקציות שמקבלות נתונים בפרמטרים ומחזירות תוצאה, במקום להסתמך על גלובליים

מה נלמד?

Scope (טווח הכרה) = האזור בקוד שבו שם (משתנה או פונקציה) מוכר. נלמד:

  • Local — משתנים בתוך פונקציה.
  • Enclosing — משתנים של פונקציה חיצונית, כשיש פונקציה בתוך פונקציה.
  • Global — משתנים ברמה הראשית של הקובץ.
  • Built-in — שמות מובנים של Python (print, len…).
  • LEGB — הסדר שבו Python מחפשת שם.

למה צריך את זה?

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

ומצד שני, שגיאות כמו NameError או UnboundLocalError מבלבלות מאוד אם לא מבינים Scope.

הסבר מהבסיס

Local — מקומי

משתנה שנוצר בתוך פונקציה (כולל הפרמטרים שלה) הוא מקומי:

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

Global — גלובלי

משתנה שנוצר ברמה הראשית של הקובץ (לא בתוך פונקציה) הוא גלובלי:

  • נגיש לקריאה מכל פונקציה בקובץ.
  • כדי לשנות אותו (להשים לו ערך חדש) מתוך פונקציה, צריך הכרזה מפורשת: global name.

Enclosing — עוטף

אפשר להגדיר פונקציה בתוך פונקציה. הפנימית רואה את המשתנים של החיצונית. ה-Scope של החיצונית נקרא Enclosing מנקודת המבט של הפנימית.

Built-in — מובנה

השמות ש-Python מכירה תמיד, בלי import: print, len, sum, max, int, str, list, range…

סדר החיפוש: LEGB

כשהקוד משתמש בשם, Python מחפשת אותו בסדר הזה, ועוצרת במקום הראשון שבו מצאה:

L  Local       → inside the current function
E  Enclosing   → inside outer functions (if nested)
G  Global      → top level of the file
B  Built-in    → Python's built-in names
              → not found anywhere: NameError
אנלוגיה: איפה המפתחות?

מחפשים מפתחות: קודם בכיס (Local), אחר כך בתיק שאתם נושאים (Enclosing), אחר כך בבית (Global), ולבסוף בלובי של הבניין, איפה שמשאירים לכולם (Built-in). ברגע שמצאתם, מפסיקים לחפש. ומפתח בכיס של חבר אתם לא רואים בכלל: זה ה-Local של פונקציה אחרת.

תחביר

counter = 0                  # global

def outer():
    message = "hi"           # local to outer = enclosing for inner

    def inner():
        result = message + "!"   # local to inner; message found in Enclosing
        return result

    return inner()

def increase():
    global counter           # declare: assignments go to the GLOBAL counter
    counter = counter + 1

דוגמת קוד ראשונה

week-2/scope_demo.py:

course = "Kodkod 6"


def welcome(name):
    message = "Welcome " + name + " to " + course
    return message


print(welcome("Dana"))
print(course)
print(message)

פלט:

Welcome Dana to Kodkod 6
Kodkod 6
Traceback (most recent call last):
  File "scope_demo.py", line 11, in <module>
    print(message)
NameError: name 'message' is not defined

פירוק הקוד שורה אחר שורה

שםScopeהסבר
courseGlobalנוצר ברמה הראשית. welcome קוראת אותו: L (לא נמצא) ‏←‏ E (אין) ‏←‏ G (נמצא!).
nameLocal של welcomeפרמטר = משתנה מקומי. קיים רק בזמן הקריאה.
messageLocal של welcomeנוצר בתוך הפונקציה ונעלם כשהיא חוזרת.
print, len…Built-inנמצאים בסוף החיפוש.
print(message) בחוץ—ברמה הראשית אין message. הוא היה Local, וכבר נעלם, ולכן NameError.

הדרך הנכונה להוציא ערך מפונקציה היא return, וזה בדיוק מה ש-welcome עושה. מי שקרא קיבל את הערך (ויכול לשמור אותו במשתנה משלו).

דוגמה נוספת

אותו שם בכמה Scopes, ו-global. week-2/scope_levels.py:

total = 100


def local_total():
    total = 5
    print("inside local_total:", total)


def read_global():
    print("inside read_global:", total)


def change_global():
    global total
    total = total + 50
    print("inside change_global:", total)


def outer():
    label = "outer"

    def inner():
        print("inner sees enclosing label:", label)

    inner()


local_total()
print("after local_total:", total)
read_global()
change_global()
print("after change_global:", total)
outer()

פלט:

inside local_total: 5
after local_total: 100
inside read_global: 100
inside change_global: 150
after change_global: 150
inner sees enclosing label: outer

פירוק:

  • local_total — ההשמה total = 5 יוצרת משתנה מקומי חדש בשם total, ש"מסתיר" את הגלובלי. הגלובלי לא השתנה (100).
  • read_global — רק קוראת total. אין לה מקומי בשם הזה, ולכן לפי LEGB מגיעים לגלובלי.
  • change_global — global total אומר ל-Python: “בפונקציה הזו, total הוא הגלובלי”. עכשיו ההשמה משנה את הגלובלי (150).
  • inner רואה את label של outer. זה ה-E ב-LEGB.

טעות נפוצה

הטעות 1: UnboundLocalError — לקרוא ואז לשנות גלובלי בלי global.

count = 0


def add_one():
    count = count + 1


add_one()
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

למה? כי בפונקציה יש השמה ל-count, Python מחליטה (מראש, לפני הריצה) ש-count הוא מקומי בכל הפונקציה. ואז count + 1 מנסה לקרוא מקומי שעוד לא קיבל ערך.

הטעות 2 (הגדולה): להשתמש ב-global כפתרון קבוע. global “עובד”, אבל:

  • כל פונקציה יכולה לשנות את המשתנה, וכשיש באג, אין דרך לדעת מי שינה.
  • הפונקציה תלויה במשהו “מבחוץ”, ולכן קשה לבדוק אותה ולהשתמש בה שוב.

הדרך הנקייה: פרמטר נכנס, return יוצא.

def add_one(count):
    return count + 1


count = 0
count = add_one(count)
print(count)   # 1

עכשיו ברור לגמרי מה נכנס ומה יוצא, והפונקציה לא נוגעת בשום דבר שלא קיבלה. (בנושא OOP נלמד דרך נוספת לנהל “מצב” שמשתנה: אובייקטים.)

הטעות 3: לדרוס שם Built-in.

list = [1, 2, 3]
numbers = list(range(5))
TypeError: 'list' object is not callable

list הגלובלי (שלנו) נמצא לפני ה-Built-in ב-LEGB, ולכן “הסתיר” את פונקציית ה-Built-in. אותו דבר עם sum = 0, max = 10, input = ..., str = .... לעולם אל תקראו למשתנים בשמות של Built-ins.

תרגול

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

תרגיל 1: המשתנה שנעלם

def welcome(name):
    message = "Welcome " + name
    return message

print(welcome("Dana"))
print(message)
  1. מה יקרה בשורה האחרונה, ולמה?
  2. תקנו כך שההודעה תודפס פעמיים.
הצג פתרון
  1. NameError: name 'message' is not defined. message הוא משתנה Local של welcome — הוא נוצר בכל קריאה ונעלם כשהפונקציה מסתיימת.
  2. שומרים את הערך שהוחזר במשתנה משלנו:
def welcome(name):
    message = "Welcome " + name
    return message


result = welcome("Dana")
print(result)
print(result)
Welcome Dana
Welcome Dana

תרגיל 2: חזו את הפלט

x = "global"

def first():
    x = "local in first"
    print("1:", x)

def second():
    print("2:", x)

first()
second()
print("3:", x)
הצג פתרון
x = "global"


def first():
    x = "local in first"
    print("1:", x)


def second():
    print("2:", x)


first()
second()
print("3:", x)
1: local in first
2: global
3: global
  • first יוצרת x מקומי משלה — הגלובלי לא נגע.
  • second לא מגדירה x, ולכן לפי LEGB מגיעים ל-Global.

תרגיל 3: UnboundLocalError

count = 0

def add_one():
    count = count + 1

add_one()
  1. איזו שגיאה תתקבל, ולמה?
  2. תקנו בלי global.
הצג פתרון
  1. UnboundLocalError. בגלל ההשמה count = ..., Python מחליטה ש-count הוא משתנה מקומי בכל הפונקציה — ואז count + 1 מנסה לקרוא מקומי שעוד אין לו ערך.
  2. פרמטר נכנס, return יוצא:
def add_one(count):
    return count + 1


count = 0
count = add_one(count)
count = add_one(count)
print(count)
2

תרגיל 4: לדרוס שם מובנה

list = [1, 2, 3]
numbers = list(range(5))
  1. איזו שגיאה תתקבל?
  2. הסבירו בעזרת LEGB למה זה קורה, ותקנו.
הצג פתרון
  1. TypeError: 'list' object is not callable.
  2. לפי LEGB, Python מחפשת את list ב-Global לפני Built-in. המשתנה שלנו בשם list “הסתיר” את הפונקציה המובנית. התיקון — לא לתת למשתנים שמות של Built-ins:
items = [1, 2, 3]
numbers = list(range(5))
print(items, numbers)
[1, 2, 3] [0, 1, 2, 3, 4]

תרגיל 5: בלי global

cart = []
total = 0

def add(price):
    global total
    cart.append(price)
    total = total + price

add(100)
add(50)
print(cart, total)

בצעו Refactoring כך שאף פונקציה לא תשתמש ב-global ולא תשנה משתנים חיצוניים: כל פונקציה מקבלת בפרמטרים את מה שהיא צריכה ומחזירה תוצאה.

פלט צפוי:

[100, 50] 150
הצג פתרון
def add(cart, price):
    return cart + [price]


def cart_total(cart):
    return sum(cart)


cart = []
cart = add(cart, 100)
cart = add(cart, 50)
print(cart, cart_total(cart))
[100, 50] 150
  • total כבר לא “מצב” שצריך לתחזק — הוא מחושב מהעגלה.
  • add מחזירה רשימה חדשה (cart + [price]) ולא משנה את הקיימת.

סיכום

  • Scope = איפה שם מוכר. משתנה שנוצר בפונקציה (כולל פרמטרים) הוא Local, ונעלם בסוף הקריאה.
  • LEGB: Local → Enclosing → Global → Built-in. נעצרים במקום הראשון שבו נמצא. אם לא נמצא, NameError.
  • השמה בתוך פונקציה יוצרת משתנה מקומי, גם אם יש גלובלי באותו שם.
  • global x מאפשר לשנות גלובלי מתוך פונקציה, אבל עדיף להימנע. פרמטר נכנס ו-return יוצא.
  • לא נותנים למשתנים שמות של Built-ins (list, sum, max, str…).

דוקומנטציה רשמית