Higher-Order Functions — פונקציות שמקבלות ומחזירות פונקציות

מטרות השיעור
  • להסביר מה זו Higher-Order Function ולמה זה אפשרי ב-Python
  • להעביר פונקציה כפרמטר — בלי סוגריים — ולקרוא לה מתוך פונקציה אחרת
  • להשתמש ב-map, filter ובפרמטר key של sorted, max ו-min
  • לכתוב פונקציה שמחזירה פונקציה

מה נלמד?

  • מה זו Higher-Order Function (פונקציה מסדר גבוה).
  • איך מעבירים פונקציה כפרמטר — ולמה בלי סוגריים.
  • הפונקציות המובנות map ו-filter.
  • הפרמטר key של sorted, max ו-min.
  • פונקציה שמחזירה פונקציה.

למה צריך את זה?

שתי פונקציות כמעט זהות:

def double_all(numbers):
    result = []
    for n in numbers:
        result.append(n * 2)
    return result


def square_all(numbers):
    result = []
    for n in numbers:
        result.append(n * n)
    return result

הלולאה זהה — רק הפעולה על כל איבר שונה. כל פעולה חדשה (חצי, פלוס 10, עיגול…) דורשת עוד פונקציה שלמה, וזו בדיוק הכפילות ש-DRY אוסר.

הרעיון: לכתוב את הלולאה פעם אחת, ולהעביר את הפעולה עצמה כפרמטר.

הסבר מהבסיס

פונקציה היא אובייקט

כבר ראינו בשיעור “פונקציות לעומק” שב-Python פונקציה היא אובייקט — כמו מספר או רשימה:

  • double (בלי סוגריים) — הפונקציה עצמה. אפשר לשמור אותה במשתנה או להעביר אותה.
  • double(5) (עם סוגריים) — קריאה לפונקציה. התוצאה היא מה שהיא מחזירה.

Higher-Order Function

Higher-Order Function היא פונקציה ש:

  • מקבלת פונקציה אחרת כפרמטר, או
  • מחזירה פונקציה.

כבר השתמשתם באחת: sorted(products, key=lambda p: p[1]) — sorted מקבלת פונקציה בפרמטר key.

אנלוגיה: מכונת כביסה עם תוכניות

מכונת כביסה יודעת “לעבור על כל הבגדים”. מה לעשות לכל בגד — את זה בוחרים בתוכנית: כביסה עדינה, רגילה או ייבוש. לא קונים מכונה חדשה לכל תוכנית — מעבירים לה את התוכנית. Higher-Order Function היא המכונה, והפונקציה שמעבירים היא התוכנית.

תחביר

def apply_to_all(numbers, action):     # action is a function
    result = []
    for n in numbers:
        result.append(action(n))       # call it for each item
    return result


apply_to_all([1, 2, 3], double)        # pass it: no parentheses
list(map(double, [1, 2, 3]))           # built-in: map
list(filter(is_even, [1, 2, 3, 4]))    # built-in: filter
sorted(words, key=len)                 # key: sort by the result of len

דוגמת קוד ראשונה

week-2/apply_to_all.py:

def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result


def double(n):
    return n * 2


print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))

פלט:

[2, 4, 6]
[1, 4, 9]
[2, 2]

פירוק הקוד שורה אחר שורה

שורההסבר
def apply_to_all(numbers, action):action הוא פרמטר רגיל — רק שמצפים שיהיה בו פונקציה.
result.append(action(n))קוראים לפונקציה שקיבלנו, על כל איבר.
apply_to_all([1, 2, 3], double)מעבירים את double בלי סוגריים — את הפונקציה, לא את התוצאה שלה.
lambda n: n * nפונקציה קצרה בלי שם, שנוצרת במקום.
roundגם פונקציה מובנית אפשר להעביר.

דוגמה נוספת

map, filter ו-key. week-2/hof_builtins.py:

prices = [100, 250, 40, 80]

with_fee = list(map(lambda p: p + 10, prices))
cheap = list(filter(lambda p: p < 100, prices))
print(with_fee)
print(cheap)

words = ["banana", "fig", "cherry", "kiwi"]
print(sorted(words, key=len))
print(max(words, key=len))

students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]
print(max(students, key=lambda s: s[1]))

פלט:

[110, 260, 50, 90]
[40, 80]
['fig', 'kiwi', 'banana', 'cherry']
banana
('Dana', 92)
  • map(func, items) — מפעילה את func על כל איבר. list(...) הופך את התוצאה לרשימה.
  • filter(func, items) — שומרת רק את האיברים ש-func מחזירה עליהם True.
  • key=len — sorted ו-max משווים לפי התוצאה של len על כל מילה, ולא לפי המילה עצמה.

פונקציה שמחזירה פונקציה. week-2/make_multiplier.py:

def make_multiplier(factor):
    def multiply(n):
        return n * factor
    return multiply


double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))
print(triple(5))

פלט:

10
15

make_multiplier היא “מפעל” שמייצר פונקציות: כל קריאה מחזירה פונקציה חדשה, שזוכרת את ה-factor שלה. איך בדיוק היא זוכרת — נראה בשיעור הבא, על פונקציות מקוננות.

טעות נפוצה

הטעות: לקרוא לפונקציה במקום להעביר אותה.

apply_to_all([1, 2], double())
TypeError: double() missing 1 required positional argument: 'n'

למה? double() עם סוגריים קורא לפונקציה מיד — בלי פרמטר — ולכן היא נכשלת עוד לפני ש-apply_to_all מתחילה. מעבירים את הפונקציה בלי סוגריים: apply_to_all([1, 2], double). הפונקציה שמקבלת אותה היא זו שתקרא לה.

תרגול

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

תרגיל 1: פונקציה כפרמטר

כתבו פונקציה apply_to_all(numbers, action) שמפעילה את action על כל מספר ומחזירה רשימה חדשה. השתמשו בה עם פונקציה רגילה double, עם lambda שמעלה בריבוע, ועם round.

print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))

פלט צפוי:

[2, 4, 6]
[1, 4, 9]
[2, 2]
הצג פתרון
def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result


def double(n):
    return n * 2


print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))
[2, 4, 6]
[1, 4, 9]
[2, 2]

מעבירים את הפונקציה בלי סוגריים (double, round). apply_to_all היא זו שקוראת לה.

תרגיל 2: map ו-filter

prices = [20, 75, 120, 45, 90]
  1. השאירו רק מחירים מעל 50, בעזרת filter.
  2. הוסיפו 10 לכל מחיר שנשאר, בעזרת map.

פלט צפוי:

[75, 120, 90]
[85, 130, 100]
הצג פתרון
prices = [20, 75, 120, 45, 90]

expensive = list(filter(lambda p: p > 50, prices))
raised = list(map(lambda p: p + 10, expensive))
print(expensive)
print(raised)
[75, 120, 90]
[85, 130, 100]

filter שומרת רק את מה שה-lambda מחזירה עליו True. map מפעילה את ה-lambda על כל איבר. list(...) הופך את התוצאה לרשימה.

תרגיל 3: key — לפי מה למיין

words = ["banana", "fig", "cherry", "kiwi"]
students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]
  1. מיינו את המילים לפי האורך.
  2. מצאו את המילה הארוכה ביותר.
  3. מיינו את הסטודנטים לפי הציון, מהגבוה לנמוך, והדפיסו רק את השמות.

פלט צפוי:

['fig', 'kiwi', 'banana', 'cherry']
banana
Dana
Noa
Avi
הצג פתרון
words = ["banana", "fig", "cherry", "kiwi"]
students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]

print(sorted(words, key=len))
print(max(words, key=len))

for name, grade in sorted(students, key=lambda s: s[1], reverse=True):
    print(name)
['fig', 'kiwi', 'banana', 'cherry']
banana
Dana
Noa
Avi

key=len — משווים לפי התוצאה של len על כל מילה, לא לפי המילה עצמה.

תרגיל 4: פונקציה שמחזירה פונקציה

כתבו פונקציה make_discount(percent) שמחזירה פונקציה, שמקבלת מחיר ומחזירה אותו אחרי ההנחה.

ten_off = make_discount(10)
half_price = make_discount(50)
print(ten_off(200))
print(half_price(200))

פלט צפוי:

180.0
100.0
הצג פתרון
def make_discount(percent):
    def apply(price):
        return price - price * percent / 100
    return apply


ten_off = make_discount(10)
half_price = make_discount(50)
print(ten_off(200))
print(half_price(200))
180.0
100.0

make_discount היא “מפעל” — כל קריאה מייצרת פונקציה חדשה שזוכרת את ה-percent שלה.

תרגיל 5: לקרוא במקום להעביר

def double(n):
    return n * 2

print(apply_to_all([1, 2], double()))
  1. איזו שגיאה תתקבל, ולמה?
  2. מה התיקון?
הצג פתרון
  1. TypeError: double() missing 1 required positional argument: 'n'. הסוגריים ב-double() קוראים לפונקציה מיד — בלי פרמטר — עוד לפני ש-apply_to_all מתחילה.
  2. מעבירים את הפונקציה עצמה, בלי סוגריים:
def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result


def double(n):
    return n * 2


print(apply_to_all([1, 2], double))
[2, 4]

סיכום

  • Higher-Order Function = פונקציה שמקבלת פונקציה או מחזירה פונקציה. זה אפשרי כי פונקציה היא אובייקט.
  • מעבירים פונקציה בלי סוגריים — הפונקציה שמקבלת אותה היא זו שקוראת לה.
  • map — פעולה על כל איבר. filter — שומרת את מה שעומד בתנאי.
  • key= ב-sorted, max, min — קובע לפי מה משווים.
  • פונקציה יכולה להחזיר פונקציה — “מפעל” שמייצר פונקציות מותאמות.

דוקומנטציה רשמית