שימוש מתקדם בפונקציות#

אג’נדה#

  • ערכי ברירת מחדל לפונקציות

  • פונקציות שמקבלות פונקציות

  • פונקציות אנונימיות (lambda)

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

פרמטרים לפונקציות עם ערכי ברירת מחדל#

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

שימו לב: פרמטרים עם ערכי ברירת מחדל תמיד צריכים להופיע אחרי פרמטרים רגילים (בלי ברירת מחדל). אחרת פייתון לא ידע איזה ערך לשייך למה.

דוגמאות#

דוגמא: לפונקציה f1 יש שני משתנים פורמליים.
הפרמטר x הוא משתנה ללא ערך ברירת מחדל, ולכן בכל קריאה לf1 המשתמש יצטרך לתת לx ערך.
לפרמטר y יש ערך ברירת מחדל של 1. אם המשתמש יקרא לפונקציה בלי להגדיר ערך לy, ההצבה בהתחלה תשים בy את הערך 1.

def f1(x, y=1):
    return x+y

במקרה הזה, המשתמש דורס את ערך ברירת המחדל של y, ולכן y יהיה שווה 2.

f1(1, 2)
3

במקרה הזה המשתמש לא הגדיר ערך לy, ולכן y יקבל את ערך ברירת המחדל 1.

f1(3)
4

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

f1()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 f1()

TypeError: f1() missing 1 required positional argument: 'x'

דוגמא: הפעם לכל המשתנים הפורמליים יש ערך ברירת מחדל!

def f2(x=1, y=2, z=3):
    return x+y+z

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

f2()
6

ומה אם היינו רוצים לדרוס רק את הערך של y? מה היה קורה אם נקרא לפונקציה עם ערך אחד?

f2(10)
15

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

f2(y=5)   # in this call: x = 1, y = 5, z = 3
9

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

f1(x=5, y=3) # x does not have a default value
8

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

def f2(x=1, y, z):   # Invalid syntax
    return x+y+z
  Cell In[12], line 1
    def f2(x=1, y, z):   # Invalid syntax
                ^
SyntaxError: non-default argument follows default argument

פונקציה כפרמטר#

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

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

def add(x, y):
    return x + y

def multiply(x, y):
    return x * y

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

def pairs_op_long(lst, op):
    results = []
    for i in range(len(lst)-1):
        if op == "add":
            answer = add(lst[i], lst[i+1])
        elif op == "multiply":
            answer = multiply(lst[i], lst[i+1])
        else:
            print("not legal operation")
            return
        results.append(answer)
    return results

להלן דוגמא לשימוש בפונקציה זו:

nums = [1, 2, 3, 4]
print(pairs_op_long(nums, "add"))
print(pairs_op_long(nums, "multiply"))
[3, 5, 7]
[2, 6, 12]

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

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

def pairs_op(lst, op):
    results = []
    for i in range(len(lst)-1):
        results.append(op(lst[i], lst[i+1]))
    return results

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

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

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

nums = [1, 2, 3, 4]
print(pairs_op(nums, add))
print(pairs_op(nums, multiply))
[3, 5, 7]
[2, 6, 12]

פונקציות אנונימיות (lambda)#

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

lambda params: the_return_value

כך אנחנו חוסכים גם ירידות שורה, אבל אפילו את הפקודה return!

דוגמא: מחשבון#

הפעם נגדיר את הפונקציות add וmultiply בתור פונקציות lambda. עדיין ניתן לתת לפונקציות הללו שם - על-ידי השמה.

add = lambda x, y: x+y
add(1,2)
3
multiply = lambda x, y:  x*y
multiply(2,4)
8

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

תזכורת לפונקציה pairs_op:

def pairs_op(lst, op):
    results = []
    for i in range(len(lst)-1):
        results.append(op(lst[i], lst[i+1]))
    return results

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

pairs_op([1,2,3,4], lambda x, y: x - y)
[-1, -1, -1]

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

שימוש בפרמטר key על מנת להגדיר יחס סדר בפונקציות מובנות#

עד כה ראינו מספר דוגמאות לפנוקציות מובנות בשפת פייתון המשתמשות ביחס סדר על איברים באוסף על מנת להחזיר תוצאה. לדוגמא, הפונקציה sorted משתמשת ביחס הסדר בין האיברים כדי להחזירם באופן סדור.
דוגמא נוספת היא הפונקציה max, המחזירה את האיבר המקסימלי, כלומר, האיבר שמבחינת הסדר שהוגדר, הוא גדול (או שווה) ביחס לשאר האיברים באוסף.

נתחיל מלבחון את הפונקציה sorted. עד עכשיו השתמשנו בה בסדר פשוט, לדוגמא:

numbers = [10, 3, 7, 1]
sorted_numbers = sorted(numbers)
print(sorted_numbers)
[1, 3, 7, 10]

כפי שראינו, sorted מחזירה רשימה ממוינת מהקטן לגדול.

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

words = ["apple", "kiwi", "banana", "fig"]
sorted_words = sorted(words, key=lambda a: len(a)) # Notice that we don't give "len()"
print(sorted_words)
['fig', 'kiwi', 'apple', 'banana']

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

באופן דומה, ניתן להשתמש בפרמטר key כדי להגדיר יחס סדר עבור הפונקציות min ו-max.
בנוסף, שימו לב שמכיוון ש-len מקבלת פרמטר אחד בלבד (בדיוק כמו פונקצית הlambda שהגדרנו למעלה) ניתן להעבירה ישירות כפונקציה ל-key:

print(min(words, key=len))
fig
print(max(words, key=len))
banana

נסו כעת לכתוב שורת קוד בודדת, המדפיסה את האיבר המינימלי מבחינת אורך, תוך שימוש בפונקציה המובנית max (זו לא טעות!) עם הארגומנט key:

רמז

השתמשו בlambda על מנת להגדיר פונקציה חדשה אשר מבצעת את הנדרש בעזרת הפונקציה len

words = ["apple", "kiwi", "banana", "fig"]
## Write your code here