2.16. תחום (Scope)

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

Nested boxes showing a local scope inside a module scope inside the built-in scope, with an arrow indicating that name lookup walks outward from the innermost frame.

חיפוש שמות מתחיל בתחום הפונקציה המקומי ומתקדם החוצה לתחומי המודול והמובנה עד שנמצאת התאמה.

2.16.1. תחום מקומי ותחום מודול

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

def f():
    x = 10
    print(x)

f()
print(x)              # NameError: x is not defined

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

CAMERA = "OpenMV"

def banner():
    print("running on", CAMERA)

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

counter = 0

def bump():
    counter = counter + 1     # UnboundLocalError

ה-counter בצד שמאל הופך את counter לשם מקומי ב-bump, ולכן לקריאה בצד ימין אין ערך למצוא.

2.16.1.1. מילת המפתח global

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

counter = 0

def bump():
    global counter
    counter += 1

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

2.16.2. Lambdas

lambda בונה פונקציה אנונימית קטנה בביטוי יחיד:

square = lambda x: x * x
square(7)             # 49

היא שווה-ערך בדיוק ל-:

def square(x):
    return x * x

גוף ה-lambda חייב להיות ביטוי יחיד – ללא פקודות, ללא שורות מרובות. השימוש העיקרי הוא העברת פונקציה זעירה כארגומנט למשהו שלוקח פונקציה:

pairs = [("b", 2), ("a", 3), ("c", 1)]
pairs.sort(key=lambda item: item[1])
# [('c', 1), ('b', 2), ('a', 3)]

כשהגוף גדל מעבר לביטוי אחד, עברו ל-def אמיתי. מתן שם לפונקציה עם def גם נותן לה שם ב-tracebacks, מה שאין ל-lambda.

2.16.3. Closures

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

def make_adder(n):
    def add(x):
        return x + n
    return add

add5 = make_adder(5)
add10 = make_adder(10)
print(add5(100), add10(100))

פלט:

105 110

add5 ו-add10 הן שתי פונקציות נפרדות, כל אחת זוכרת את ה-n שלה. פונקציה שבונה ומחזירה פונקציה פנימית מותאמת בדרך זו נקראת closure. זוהי הסיבה העיקרית שבגללה שפה זקוקה לפונקציות מקוננות מלכתחילה – דרך לאפות מצב כלשהו לתוך ערך פונקציה ואז למסור את הערך הזה כאובייקט יחיד הניתן לקריאה.

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

def make_counter():
    count = 0
    def tick():
        count = count + 1     # UnboundLocalError
        return count
    return tick

ההשמה ל-count בתוך tick הופכת את count למקומי ל-tick, באותו אופן שבו הוא היה הופך אותו למקומי בפונקציה ברמה העליונה. מילת המפתח nonlocal אומרת ל-Python ”השם הזה חי בפונקציה העוטפת, קְשור אותו מחדש שם“:

def make_counter():
    count = 0
    def tick():
        nonlocal count
        count += 1
        return count
    return tick

c = make_counter()
print(c(), c(), c())

פלט:

1 2 3

nonlocal הוא לתחום הפונקציה העוטפת מה ש-global הוא לתחום המודול. שימו לב ששינוי אובייקט שנלכד (קריאה ל-some_list.append(...), some_dict[k] = v) אינו זקוק ל-nonlocal – השם אינו נקשר מחדש, רק האובייקט שאליו הוא מצביע משתנה.