ארבעת התחומים (scope) של פייתון

מחבר:
בתאריך:

אחד הדברים הכי חשובים למתכנת בכל שפת תכנות הוא התחום של משתנים (scope) שמאפשר לגשת למשתנים בהתאם לתחום שבו הם מוגדרים.

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

  1. Local
  2. Enclosing
  3. Global
  4. Built-in

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

ארבעת התחומים (scope) של פייתון

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

 

תחום מקומי (לוקלי) לעומת גלובלי

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

ניתן לגשת למשתנים בתוך הפונקציה. לדוגמה, המשתנה x המוגדר בתוך הפונקציה fun:

def fun():
    # local scope
    x = 'local x'
    print(x)
fun()

והתוצאה היא:

local x

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

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

נדפיס את המשתנה הגלובלי y באמצעות פקודת print שנמצאת אף היא בתחום הגלובלי. ואת המשתנה x שנמצא בתוך הפונקציה נדפיס באמצעות פקודת print שנמצאת בתוך הפונקציה.

# global scope
y = 'global y'

def fun():
    # local scope
    x = 'local x'
    print(x)
fun() print(y)

התוצאה היא:

local x
global y

מדוע?

  • הפונקציה fun מדפיסה את המשתנה x שנמצא בתוכה.
  • המשתנה y קיים בתחום הגלובלי ומודפס בתחום הגלובלי.

עכשיו, שימו לב לנקודה עקרונית!

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

זו נקודה כל כך חשובה, ולכן אחזור עליה יותר בגדול:

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

# global scope
y = 'global y'

def fun():
    print(y)

fun()
global y

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

מה קורה אם יש לנו שני משתנים בעלי אותו שם. אחד בתחום הגלובלי ואחד בלוקלי?

# global scope
y = 'global y'

def fun():
    # local scope
    y = 'local y'
    print(y)
fun() print(y)
local y
global y

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

 

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

def fun():
    y = 'global y'

fun()

print(y)
Traceback (most recent call last):
  File "main.py", line 6, in 
    print(y)
NameError: name 'y' is not defined

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

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

def fun():
    global y
    y = 'global y'

fun()

print(y)

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

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

 

תחום enclosing - פונקציה בתוך פונקציה

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

def fun_outer():
    x = 'outer x'

    def fun_inner():
        x = 'inner x'
        print(x)

    fun_inner()
    print(x)

fun_outer()

והתוצאה:

inner x
outer x

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

def fun_outer():
    x = 'outer x'

    def fun_inner():
        #x = 'inner x'
        print(x)

    fun_inner()
    print(x)

fun_outer()
outer x
outer x

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

הקוד הבא לא יעבוד.

def fun_outer():
    #x = 'outer x'

    def inner():
        x = 'inner x'
        print(x)

    fun_inner()
    print(x)

fun_outer()
Traceback (most recent call last):
  File "main.py", line 11, in 
    fun_outer()
  File "main.py", line 8, in fun_outer
    fun_inner()
NameError: name 'fun_inner' is not defined

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

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

def fun_outer():
    x = ''

    def fun_inner():
        nonlocal x
        x = 'inner x'
        print(x)

    fun_inner()
    print(x)

fun_outer()

זה עובד. בגלל שתי סיבות:

  1. בזכות הגדרת המשתנה בתוך הפונקציה כ-nonlocal
  2. עוד דבר שחייבים לעשות הוא להגדיר את המשתנה גם בתוך הפונקציה העוטפת כדי שפייתון לא יזרוק הודעת שגיאה.
inner x
inner x

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

ברוב המקרים, אנחנו נרצה להשאיר את המשתנים של הפונקציה הפנימית מקומיים בלי אפשרות לדרוס את הקוד שמחוץ לפונקציה. ובכל זאת, חשוב להכיר את מילת המפתח nonlocal אם במקרה תתקלו בה בקוד שכתב מישהו אחר.

 

תחום built-in

פייתון בא עם מספר פונקציות מובנות (built-in). לדוגמה, הפונקציה print שמדפיסה אובייקטים.

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

def print(str):
    pass

print('hi')

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

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

import builtins

print(dir(builtins)) 
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

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

 

סיכום

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

סדר גישה

שם התחום

משמעות

1

Local מקומי

התחום בתוך הפונקציה

2

Enclosing

תחום פונקציה חיצונית שעוטף פונקציה פנימית

3

Global גלובלי

התחום מחוץ לפונקציות

4

Built-in

פונקציות מובנות (בילט-אין) של פייתון

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

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

x = 'global x'

def fun_outer():
    x = 'outer x'

    def fun_inner():
        x = 'inner x'
        print(x)

    fun_inner()
    print(x)

fun_outer()
print(x)

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

התוצאה בהתאם היא:

inner x
outer x
global x

מה ידפיס הקוד הבא?

x = 'global x'

def fun_outer():
    x = 'outer x'

    def fun_inner():
        #x = 'inner x'
        print(x)

    fun_inner()
    print(x)

fun_outer()
print(x)
outer x
outer x
global x

ומה לגבי הקוד הבא?

x = 'global x'

def fun_outer():
    #x = 'outer x'

    def fun_inner():
        #x = 'inner x'
        print(x)

    fun_inner()
    print(x)

fun_outer()
print(x)
global x
global x
global x

לכל המדריכים בסדרה ללימוד פייתון

 

אהבתם? לא אהבתם? דרגו!

0 הצבעות, ממוצע 0 מתוך 5 כוכבים

 

 

הוסף תגובה חדשה

 

= 6 + 3