העשרה: Closures לעומק
מוטיבציה
בסיכום פונקציות מסדר גבוה ראינו closures בקצרה. כאן נצלול לעומק — nonlocal, __closure__, מלכודות נפוצות, ושימוש ב-closure כ-state פרטי.
תזכורת: מה זה Closure?
Closure = פונקציה פנימית ש"זוכרת" משתנים מהפונקציה החיצונית, גם אחרי שהחיצונית סיימה לרוץ.
def make_multiplier(n):
def multiplier(x):
return x * n # n "נזכר" מ-make_multiplier
return multiplier
double = make_multiplier(2)
double(5) # 10 — n=2 עדיין חי!
nonlocal — שינוי משתנים מהסביבה
קריאה vs שינוי
קריאה — עובדת בלי שום הכרזה:
def outer():
x = 10
def inner():
print(x) # OK — קוראים x מ-outer
inner()
outer() # 10
שינוי — בלי nonlocal, פייתון יוצר משתנה מקומי חדש:
def outer():
x = 10
def inner():
x = 20 # יוצר x חדש ב-inner, לא משנה את outer!
inner()
print(x)
outer() # 10 — לא השתנה!
עם nonlocal — עכשיו באמת משנים:
def outer():
x = 10
def inner():
nonlocal x
x = 20 # משנה את x של outer
inner()
print(x)
outer() # 20 — השתנה!
nonlocal vs global
nonlocal | global | |
|---|---|---|
| מתייחס ל... | משתנה בפונקציה העוטפת | משתנה ברמת המודול |
| שימוש | closures | מצב גלובלי (נדיר) |
| דוגמה | פונקציה בתוך פונקציה | פונקציה שמשנה משתנה חיצוני |
counter = 0 # משתנה גלובלי
def outer():
count = 0 # משתנה של outer
def inner():
nonlocal count # מתייחס ל-count של outer
count += 1
global counter # מתייחס ל-counter הגלובלי
counter += 1
__closure__ — מה פייתון שומר?
בדיקת ה-closure
def make_adder(n):
def adder(x):
return x + n
return adder
f = make_adder(5)
# בדיקה שיש closure
print(f.__closure__) # (<cell ...>,)
print(f.__closure__[0].cell_contents) # 5
מה קורה?
- כל משתנה "חופשי" (free variable) שהפונקציה הפנימית משתמשת בו נשמר ב-cell object
- ה-cell מכיל reference לאובייקט, לא עותק
def make_pair(a, b):
def get_pair():
return (a, b)
return get_pair
f = make_pair("hello", 42)
print(len(f.__closure__)) # 2 (שני משתנים חופשיים)
print(f.__closure__[0].cell_contents) # "hello"
print(f.__closure__[1].cell_contents) # 42
פונקציה רגילה — אין closure
def regular(x):
return x + 1
print(regular.__closure__) # None — אין משתנים חופשיים
מלכודת קריטית: reference, לא עותק!
Closure שומרת reference לאובייקט
def f(L):
def g(x):
return L[0] == x
return g
L1 = [1, 2, 3]
my_g = f(L1)
print(my_g(1)) # True — L[0] == 1
L1[0] = 100 # שינוי (mutation) של הרשימה
print(my_g(1)) # False! — L בתוך g מצביע לאותה רשימה
print(my_g(100)) # True
mutation vs rebinding
def f(L):
def g(x):
return L[0] == x
return g
L2 = [1, 2, 3]
my_g = f(L2)
L2 = [99, 88, 77] # rebinding — L2 מצביע לרשימה חדשה
print(my_g(1)) # True! — L ב-closure עדיין מצביע לרשימה הישנה [1,2,3]
הכלל
| פעולה | ה-closure מושפע? | למה? |
|---|---|---|
Mutation (שינוי תוכן): L[0] = 100 | כן | אותו אובייקט |
Rebinding (הצבה חדשה): L = [99] | לא | אובייקט חדש, ה-closure עדיין מצביע לישן |
זו שאלת בחינה קלאסית! ראו שאלה 15 ב-misc_topics.md
Closure כ-State פרטי
דוגמה: מונה (Counter)
def make_counter(start=0):
count = start
def increment():
nonlocal count
count += 1
return count
def get():
return count
def reset():
nonlocal count
count = start
return increment, get, reset
inc, get, reset = make_counter()
inc() # 1
inc() # 2
inc() # 3
get() # 3
reset()
get() # 0
למה זה שימושי?
- ה-
countהוא פרטי — אי אפשר לגשת אליו ישירות מבחוץ - רק דרך הפונקציות
increment,get,reset - זה כמו אובייקט עם מצב פנימי, בלי מחלקה!
דוגמה: צובר (Accumulator)
def make_accumulator():
total = 0
def add(n):
nonlocal total
total += n
return total
return add
acc = make_accumulator()
acc(10) # 10
acc(5) # 15
acc(3) # 18
הקשר לדקורטורים
דקורטורים כמו @memoize משתמשים ב-closure כדי לשמור את ה-cache:
def memoize(func):
cache = {} # state פרטי!
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
מלכודת משתני לולאה (Loop Variable Trap)
הבעיה
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2] — לא [0, 1, 2]!
למה?
כל שלוש ה-lambdas שומרות reference למשתנה i (לא לערך שלו). כש-i מגיע ל-2 בסוף הלולאה, כולן רואות 2.
הפתרון: ערך ברירת מחדל
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # i=i "מצלם" את הערך הנוכחי
print([f() for f in funcs]) # [0, 1, 2] — נכון!
למה עובד?
ערך ברירת מחדל מחושב בזמן הגדרת הפונקציה, לא בזמן הקריאה. אז i=i שומר עותק של הערך הנוכחי.
דוגמה נוספת עם closures מפורשות
# שגוי
def make_funcs():
funcs = []
for i in range(3):
def f():
return i # i הוא reference!
funcs.append(f)
return funcs
[f() for f in make_funcs()] # [2, 2, 2]
# נכון — closure שלוכדת את הערך
def make_funcs():
funcs = []
for i in range(3):
def f(n=i): # n=i "מצלם"
return n
funcs.append(f)
return funcs
[f() for f in make_funcs()] # [0, 1, 2]
סיכום נקודות חשובות
- Closure = פונקציה + סביבה שנלכדה (captured environment)
-
nonlocalמאפשר לשנות משתנים מהפונקציה העוטפת -
__closure__מכיל cell objects עם references - Closure שומרת reference, לא עותק — mutation משפיעה, rebinding לא
- מלכודת הלולאה:
lambda: iתמיד רואה את i האחרון → פתרון:lambda i=i: i - Closures מאפשרות state פרטי — כמו אובייקט מינימלי
- דקורטורים משתמשים ב-closures לשמירת state (cache, counters)
קישורים נוספים
- בסיס: higher_order_functions.md - closures בסיסי + דקורטורים
- שאלות בחינה: misc_topics.md - שאלה 15 (Closures ו-Aliasing)