שלום אורח התחבר

תחום הגדרה ב Python: הפקודות global ו nonlocal

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

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

1הצהרה לעומת השמה

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

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

# Declaration and initialization of new variables to new data
x = 10
y = [1, 2, 3]
z = 'hello world'

# Assigning new values to existing variables
x = 50
y = { 'a': 10 }
z = 'bye bye'

בפעם הראשונה ששמנו ערך ב x יצרנו משתנה חדש, ובפעם השניה עדכנו את הערך של משתנה x הקיים.

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

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

x = [10, 20, 30]
x[0] = 'hello'

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

NameError: name 'x' is not defined

2תחום ההגדרה הרגיל

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

כך נראית הגדרת משתנה בתחום הגדרה גלובלי:

# global variable declaration
x = 10
y = 20

# x = 10
print("x = ",x)

# y = 20
print("y = ",y)

וכך הגדרת משתנה בתוך פונקציה:

# function variable declaration
def foo():
    x = 10
    print(x)

# NameError: name 'x' is not defined
print(x)

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

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

x = 10

def foo():
    x = 20
    x += 1
    # prints 21
    print(x)

foo()
# prints 10 - variable is not affected by the function
print(x)

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

x = [1,2,3]

def foo():
    x[0] = 10
    # prints [10, 2, 3]
    print(x)

foo()

# prints [10, 2, 3]
print(x)

3חידת תחום הגדרה (1)

נסו לחשוב מה תדפיס התוכנית הבאה:

funcs = {
        idx: lambda: print(idx) for idx in range(4)
        }

funcs[0]()

התוכנית יוצרת Dictionary בו המפתחות הם המספרים 0, 1, 2 ו-3 ולכל מספר הערך הוא פונקציה שמדפיסה את האינדקס. מסתבר שהפעלה של כל אחת מארבעת הפונקציות מדפיסה את אותו הערך 3.

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

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

idx = 0
def f1():
    print(idx)

idx = 1
def f2():
    print(idx)

# prints: 1
f1()

המילה idx בתוך הפונקציות מתיחסת למשתנה הגלובלי idx.

4מילת המפתח global

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

_total = 0

def add(x):
    _total += x

# Error: UnboundLocalError: local variable '_total' referenced before assignment
add(1)
add(2)
add(4)

דרך התמודדות אחת היא להגדיר את המשתנה _total כמערך, או יותר טוב להשתמש בכתיב מונחה עצמים:

class Accumulator:
    def __init__(self):
        self.total = 0

    def add(self, x):
        self.total += x

a = Accumulator()
a.add(1)
a.add(2)
a.add(4)

# This works. Prints 7
print(a.total)

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

_total = 0

def add(x):
    global _total
    _total += x

add(1)
add(2)
add(4)

# OK - prints 7
print(_total)

5מילת המפתח nonlocal

עד לכאן פייתון נותנת מענה דומה לזה ש C נתנה לתחומי הגדרה. אבל לפייתון יש יכולת נוספת שאין לשפת C והיא היכולת להגדיר פונקציות בצורה מקוננת (כלומר פונקציה בתוך פונקציה). לדוגמא:

def calc(x):
    def twice(num):
        return num * 2

    return twice(x) + 5

# Prints 25
print(calc(10))

# NameError: name 'twice' is not defined
twice(5)

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

עכשיו נשאלת השאלה מה אם הפונקציה twice מעוניינת לגשת למשתנה x שהועבר לפונקציה calc? במצב כזה אנחנו חוזרים לבעיה דומה לזו שכבר היינו בה כשרצינו לגשת למשתנים גלובליים. קריאה או שינוי ערך אפשרית בקלות כמו בדוגמא הבאה:

def calc(x):
    def twice():
        return x * 2

    return twice() + 5

# Prints 25
print(calc(10))

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

def calc(x):
    z = 10
    def twice():
        # UnboundLocalError: local variable 'z' referenced before assignment
        z *= 20

    twice()
    twice()
    twice()
    return z + 10

print(calc(10))

כאן המילה global כבר לא תעזור לנו. המשתנה z שהוגדר בתחילת calc אינו משתנה גלובלי. זהו משתנה מקומי של פונקציה אחרת.

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

def calc(x):
    z = 10
    def twice():
        nonlocal z
        z *= 20

    twice()
    twice()
    twice()
    return z + 10

# Prints 80010
print(calc(10))

6הערות וקריאה נוספת

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

  2. המסמך PEP 3104 מתאר את הסיבות ליצירת המילה nonlocal ומשמעותה לעומק.

  3. ההסבר כאן מכסה 2 מתוך 10 הטעויות הנפוצות בפייתון. לדעתי שווה לקרוא גם את ה-8 האחרות מהקישור.


נהניתם מהפוסט? מוזמנים לשתף ולהגיב