Nested Functions, Closures ו-nonlocal
- להגדיר פונקציה בתוך פונקציה ולהסביר מה היא רואה מה-Enclosing Scope
- להסביר מה זה Closure — פונקציה שזוכרת משתנים גם אחרי שהפונקציה החיצונית הסתיימה
- להבין למה שינוי משתנה חיצוני גורם ל-UnboundLocalError, ולתקן עם nonlocal
- להבחין בין global ל-nonlocal
מה נלמד?
- Nested Function — פונקציה שמוגדרת בתוך פונקציה.
- Enclosing — ה-E של LEGB, מקרוב.
- Closure — פונקציה ש"זוכרת" משתנים.
nonlocal— איך משנים משתנה של הפונקציה העוטפת.
למה צריך את זה?
- פונקציית עזר פרטית: לפעמים פונקציה צריכה פונקציית עזר קטנה ששייכת רק לה. אין סיבה שהיא תהיה גלובלית ותתערבב עם שאר הקוד.
- פונקציות שזוכרות מצב: בשיעור הקודם ראינו ש-
make_multiplier(2)מחזירה פונקציה ש"זוכרת" את המספר 2. היום נבין איך זה עובד — ואיך בונים מונה, מצבר או מטמון בליglobal.
הסבר מהבסיס
פונקציה בתוך פונקציה
אפשר להגדיר def בתוך def. הפונקציה הפנימית:
- קיימת רק בתוך הפונקציה החיצונית — מבחוץ אי אפשר לקרוא לה.
- רואה את המשתנים והפרמטרים של הפונקציה החיצונית.
Enclosing — ה-E של LEGB
בשיעור על Scope למדנו את סדר החיפוש L → E → G → B. כשפונקציה פנימית משתמשת בשם, Python מחפשת:
L Local → inside the inner function
E Enclosing → inside the outer function
G Global → top level of the file
B Built-in → print, len, ...Closure — פונקציה שזוכרת
כשפונקציה חיצונית מחזירה את הפונקציה הפנימית, משהו מעניין קורה: הפונקציה החיצונית כבר הסתיימה, אבל הפנימית עדיין זוכרת את המשתנים שלה. הצירוף הזה — פונקציה + המשתנים שהיא זוכרת — נקרא Closure.
הפונקציה החיצונית כותבת מכתב (הפונקציה הפנימית) ומכניסה איתו למעטפה פתק עם כמה נתונים (המשתנים). היא שולחת את המעטפה ונעלמת — אבל מי שפותח את המעטפה מקבל גם את המכתב וגם את הפתק. כל מעטפה עם פתק משלה.
nonlocal
לקרוא משתנה מה-Enclosing — מותר תמיד. לשנות אותו (להשים לו ערך חדש) — אי אפשר בלי הכרזה, בדיוק כמו עם גלובלי. ההכרזה היא nonlocal name: “המשתנה הזה שייך לפונקציה העוטפת”.
| הכרזה | המשתנה שייך ל… |
|---|---|
global x | רמת הקובץ |
nonlocal x | הפונקציה העוטפת הקרובה |
תחביר
def outer():
count = 0 # enclosing variable
def inner():
nonlocal count # assignments go to outer's count
count += 1
return count
return inner # return the function itself (no parentheses)דוגמת קוד ראשונה
פונקציית עזר פרטית. week-2/receipt.py:
def format_receipt(items):
def line(name, price):
return name.ljust(10) + str(price)
lines = []
for name, price in items:
lines.append(line(name, price))
return "\n".join(lines)
print(format_receipt([("Milk", 6), ("Bread", 9)]))פלט:
Milk 6
Bread 9פירוק הקוד שורה אחר שורה
| שורה | הסבר |
|---|---|
def line(name, price): | פונקציה פנימית — קיימת רק בתוך format_receipt. |
name.ljust(10) | מרפד את השם ברווחים עד 10 תווים, כדי שהמחירים יעמדו בטור. |
lines.append(line(name, price)) | הפונקציה החיצונית משתמשת בפונקציית העזר שלה. |
"\n".join(lines) | מחבר את כל השורות לטקסט אחד, עם ירידת שורה ביניהן. |
דוגמה נוספת
Closure — פונקציה שזוכרת. week-2/greeter.py:
def make_greeter(greeting):
def greet(name):
return greeting + ", " + name
return greet
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))פלט:
Hello, Dana
Shalom, Avimake_greeter הסתיימה כבר בשורה hello = ..., אבל hello עדיין זוכרת ש-greeting הוא "Hello". לכל פונקציה שנוצרה יש עותק משלה של המשתנה — shalom זוכרת "Shalom".
מונה עם nonlocal. week-2/counter.py:
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter(), counter(), counter())
other = make_counter()
print(other())פלט:
1 2 3
1כל מונה זוכר את ה-count שלו: other מתחיל מאפס, בלי קשר ל-counter. זה מצב פרטי — בלי משתנה גלובלי שכל הקוד יכול לשנות.
טעות נפוצה
הטעות: לשנות משתנה של הפונקציה העוטפת בלי nonlocal.
def make_counter():
count = 0
def increment():
count += 1
return count
return increment
counter = make_counter()
counter()UnboundLocalError: cannot access local variable 'count' where it is not associated with a valueלמה? בגלל ההשמה count += 1, Python מחליטה ש-count הוא משתנה מקומי של increment — ואז מנסה לקרוא מקומי שעוד אין לו ערך. זו בדיוק אותה שגיאה שראינו עם גלובליים בשיעור על Scope.
התיקון: nonlocal count בתחילת increment.
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
תרגיל 1: פונקציית עזר פנימית
כתבו פונקציה format_receipt(items) שמקבלת רשימה של זוגות (שם, מחיר) ומחזירה טקסט של קבלה. בתוכה הגדירו פונקציה פנימית line(name, price) שמרפדת את השם ל-10 תווים (ljust).
print(format_receipt([("Milk", 6), ("Bread", 9)]))פלט צפוי:
Milk 6
Bread 9הצג פתרון
def format_receipt(items):
def line(name, price):
return name.ljust(10) + str(price)
lines = []
for name, price in items:
lines.append(line(name, price))
return "\n".join(lines)
print(format_receipt([("Milk", 6), ("Bread", 9)]))Milk 6
Bread 9line קיימת רק בתוך format_receipt — היא פונקציית עזר פרטית, ולא מתערבבת עם שאר הקוד.
תרגיל 2: Closure
כתבו make_greeter(greeting) שמחזירה פונקציה שמקבלת שם ומחזירה ברכה.
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))פלט צפוי:
Hello, Dana
Shalom, Aviהצג פתרון
def make_greeter(greeting):
def greet(name):
return greeting + ", " + name
return greet
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))Hello, Dana
Shalom, Avimake_greeter כבר הסתיימה, אבל כל פונקציה שהיא החזירה זוכרת את ה-greeting שלה. זה Closure.
תרגיל 3: חזו את הפלט
def outer():
message = "outer"
def show():
print("show:", message)
def change():
nonlocal message
message = "changed"
show()
change()
show()
outer()הצג פתרון
def outer():
message = "outer"
def show():
print("show:", message)
def change():
nonlocal message
message = "changed"
show()
change()
show()
outer()show: outer
show: changedchange משתמשת ב-nonlocal, ולכן ההשמה משנה את ה-message של outer — היא לא יוצרת משתנה מקומי חדש.
תרגיל 4: מונה עם צעד
כתבו make_counter(step) — כל קריאה למונה מגדילה אותו ב-step ומחזירה את הערך החדש. צרו שני מונים ובדקו שהם לא משפיעים זה על זה.
by_two = make_counter(2)
by_ten = make_counter(10)
print(by_two(), by_two())
print(by_ten())
print(by_two())פלט צפוי:
2 4
10
6הצג פתרון
def make_counter(step):
count = 0
def increment():
nonlocal count
count += step
return count
return increment
by_two = make_counter(2)
by_ten = make_counter(10)
print(by_two(), by_two())
print(by_ten())
print(by_two())2 4
10
6stepרק נקרא בתוךincrement, ולכן לא צריך עבורו הכרזה.countמשתנה, ולכן צריךnonlocal count.
תרגיל 5: בלי nonlocal
def make_counter():
count = 0
def increment():
count += 1
return count
return increment
counter = make_counter()
counter()- איזו שגיאה תתקבל, ולמה?
- מה ההבדל בין
globalל-nonlocal?
הצג פתרון
UnboundLocalError. בגלל ההשמהcount += 1, Python מחליטה ש-countהוא משתנה מקומי שלincrement— ואין לו ערך כשמנסים להגדיל אותו. התיקון:nonlocal countבתחילתincrement.global— המשתנה שייך לרמת הקובץ.nonlocal— המשתנה שייך לפונקציה העוטפת הקרובה. Closure עםnonlocalנותן מצב פרטי לכל מונה, בלי משתנה גלובלי שכל הקוד יכול לשנות.
סיכום
- Nested Function — פונקציה בתוך פונקציה. קיימת רק בפנים, ורואה את המשתנים של העוטפת (Enclosing).
- Closure — פונקציה פנימית שהוחזרה, וזוכרת את המשתנים של הפונקציה שיצרה אותה. לכל אחת עותק משלה.
- לקרוא משתנה מה-Enclosing — מותר. לשנות אותו — רק עם
nonlocal, אחרתUnboundLocalError. global— משתנה ברמת הקובץ.nonlocal— משתנה בפונקציה העוטפת.- Closure נותן מצב פרטי בלי משתנה גלובלי. ועדיין — כשאפשר, עדיף פרמטר נכנס ו-
returnיוצא.
דוקומנטציה רשמית
- Scopes and Namespaces Example — local, nonlocal ו-global בדוגמה אחת
- The nonlocal statement — nonlocal