• בלוג
  • גם בפייתון: לא מומלץ להגדיר פונקציות בלולאה

גם בפייתון: לא מומלץ להגדיר פונקציות בלולאה

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

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

1. הקוד

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

actions = {}

for idx, act in enumerate(['one', 'two', 'three', 'four']):
    actions[act] = lambda: print(idx)

actions['one']()

אבל השורה האחרונה מדפיסה 3 במקום 0. נסו לחשוב למה ואז המשיכו להסבר.

2. מה קרה שם?

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

actions = {}

for idx, act in enumerate(['one', 'two', 'three', 'four']):
    actions[act] = lambda: print(idx)
print(idx)

actions['one']()

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

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

actions['two']()
actions['three']()
actions['four']()

3. איך לתקן

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

actions = {}

def create_action(i):
    return lambda: print(i)

for idx, act in enumerate(['one', 'two', 'three', 'four']):
    actions[act] = create_action(idx)
print(idx)

# prints 0
actions['one']()

# prints 1
actions['two']()

# prints 2
actions['three']()

# prints 3
actions['four']()

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

4. אגב- לרובי אין את הבעיה הזאת

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

actions = {}

%w(one two three four).each_with_index do |item, index|
  actions[item] = proc { puts index }
end

# prints 0
actions['one'].call

# prints 1
actions['two'].call

# prints 2
actions['three'].call

# prints 3
actions['four'].call