התנהגות של פונקציות

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

שם של פונקציה

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


In [ ]:
def square(x):
    return x ** 2

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


In [ ]:
type(square)

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


In [ ]:
ribua = square

print(square(5))
print(ribua(5))

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


In [ ]:
ribua is square

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

פונקציות במבנים מורכבים

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


In [ ]:
def add(num1, num2):
    return num1 + num2


def subtract(num1, num2):
    return num1 - num2


def multiply(num1, num2):
    return num1 * num2


def divide(num1, num2):
    return num1 / num2


functions = [add, subtract, multiply, divide]

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


In [ ]:
# Option 1
print(add(5, 2))

# Option 2
math_function = functions[0]
print(math_function(5, 2))

# Option 3 (ugly, but works!)
print(functions[0](5, 2))

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


In [ ]:
for function in functions:
    print(function(5, 2))

בכל איטרציה של לולאת ה־for, המשתנה function עבר להצביע על הפונקציה הבאה מתוך רשימת functions.
בשורה הבאה קראנו לאותה הפונקציה ש־function מצביע עליה, והדפסנו את הערך שהיא החזירה.

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

תרגיל ביניים: סוגרים חשבון

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

העברת פונקציה כפרמטר

נמשיך ללהטט בפונקציות.

פונקציה נקראת "פונקציה מסדר גבוה" (higher order function) אם היא מקבלת כפרמטר פונקציה.
ניקח לדוגמה את הפונקציה calculate:


In [ ]:
def calculate(function, num1, num2):
    return function(num1, num2)

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


In [ ]:
calculate(divide, 5, 2)

מה שמתרחש במקרה הזה הוא שהעברנו את הפונקציה divide כארגומנט ראשון.
הפרמטר function בפונקציה calculate מצביע כעת על פונקציית החילוק שהגדרנו למעלה.
מכאן, שהפונקציה תחזיר את התוצאה של divide(5, 2) – הרי היא 2.5.

תרגיל ביניים: מפה לפה

כתבו generator בשם apply שמקבל כפרמטר ראשון פונקציה (func), וכפרמטר שני iterable (iter).
עבור כל איבר ב־iterable, ה־generator יניב את האיבר אחרי שהופעלה עליו הפונקציה func, דהיינו – func(item).

ודאו שהרצת התא הבא מחזירה True עבור הקוד שלכם:


In [ ]:
def square(number):
    return number ** 2


square_check = apply(square, [5, -1, 6, -8, 0])
tuple(square_check) == (25, 1, 36, 64, 0)

סיכום ביניים

וואו. זה היה די משוגע.

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

פונקציות מסדר גבוה בפייתון

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

הפונקציה map

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

במילים אחרות, map יוצרת iterable חדש.
ה־iterable כולל את הערך שהוחזר מהפונקציה עבור כל איבר ב־iterable שהועבר.

לדוגמה:


In [ ]:
squared_items = map(square, [1, 6, -1, 8, 0, 3, -3, 9, -8, 8, -7])
print(tuple(squared_items))

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

למעשה, אפשר להגיד ש־map שקולה לפונקציה הבאה:


In [ ]:
def my_map(function, iterable):
    for item in iterable:
        yield function(item)

הנה דוגמה נוספת לשימוש ב־map:


In [ ]:
numbers = [(2, 4), (1, 4, 2), (1, 3, 5, 6, 2), (3, )]
sums = map(sum, numbers)
print(tuple(sums))

במקרה הזה, בכל מעבר, קיבלה הפונקציה sum איבר אחד מתוך הרשימה – tuple.
היא סכמה את האיברים של כל tuple שקיבלה, וכך החזירה לנו את הסכומים של כל ה־tuple־ים – זה אחרי זה.

ודוגמה אחרונה:


In [ ]:
def add_one(number):
    return number + 1


incremented = map(add_one, (1, 2, 3))
print(tuple(incremented))

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

כתבו פונקציה שמקבלת רשימת מחרוזות של שתי מילים: שם פרטי ושם משפחה.
הפונקציה תשתמש ב־map כדי להחזיר מכולן רק את השם הפרטי.

חשוב!
פתרו לפני שתמשיכו!

הפונקציה filter

הפונקציה filter מקבלת פונקציה כפרמטר ראשון, ו־iterable כפרמטר שני.
filter מפעילה על כל אחד מאיברי ה־iterable את הפונקציה, ומחזירה את האיבר אך ורק אם הערך שחזר מהפונקציה שקול ל־True.
אם ערך ההחזרה שקול ל־False – הערך "יבלע" ב־filter ולא יחזור ממנה.

במילים אחרות, filter יוצרת iterable חדש ומחזירה אותו.
ה־iterable כולל רק את האיברים שעבורם הפונקציה שהועברה החזירה ערך השקול ל־True.

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


In [ ]:
def is_mature(age):
    return age >= 18

נגדיר רשימת גילים, ונבקש מ־filter לסנן אותם לפי הפונקציה שהגדרנו:


In [ ]:
ages = [0, 1, 4, 10, 20, 35, 56, 84, 120]
mature_ages = filter(is_mature, ages)
print(tuple(mature_ages))

כפי שלמדנו, filter מחזירה לנו רק גילים השווים ל־18 או גדולים ממנו.

נחדד שהפונקציה שאנחנו מעבירים ל־filter לא חייבת להחזיר בהכרח True או False.
הערך 0, לדוגמה, שקול ל־False, ולכן filter תסנן כל ערך שעבורו הפונקציה תחזיר 0:


In [ ]:
to_sum = [(1, -1), (2, 5), (5, -3, -2), (1, 2, 3)]
sum_is_not_zero = filter(sum, to_sum)
print(tuple(sum_is_not_zero))

בתא האחרון העברנו ל־filter את sum כפונקציה שאותה אנחנו רוצים להפעיל, ואת to_sum כאיברים שעליהם אנחנו רוצים לפעול.
ה־tuple־ים שסכום איבריהם היה 0 סוננו, וקיבלנו חזרה iterator שהאיברים בו הם אך ורק אלו שסכומם שונה מ־0.

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


In [ ]:
to_sum = [0, "", None, 0.0, True, False, "Hello"]
equivalent_to_true = filter(None, to_sum)
print(tuple(equivalent_to_true))

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

חשוב!
פתרו לפני שתמשיכו!

פונקציות אנונימיות

תעלול נוסף שנוסיף לארגז הכלים שלנו הוא פונקציות אנונימיות (anonymous functions).
אל תיבהלו מהשם המאיים – בסך הכול פירושו הוא "פונקציות שאין להן שם".

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


In [ ]:
def add(num1, num2):
    return num1 + num2

ונגדיר את אותה הפונקציה בדיוק בצורה אנונימית:


In [ ]:
add = lambda num1, num2: num1 + num2

print(add(5, 2))

לפני שנסביר איפה החלק של ה"פונקציה בלי שם" נתמקד בצד ימין של ההשמה.
כיצד מנוסחת הגדרת פונקציה אנונימית?

  1. הצהרנו שברצוננו ליצור פונקציה אנונימית בעזרת מילת המפתח lambda.
  2. מייד אחריה, ציינו את שמות כל הפרמטרים שהפונקציה תקבל, כשהם מופרדים בפסיק זה מזה.
  3. כדי להפריד בין רשימת הפרמטרים לערך ההחזרה של הפונקציה, השתמשנו בנקודתיים.
  4. אחרי הנקודתיים, כתבנו את הביטוי שאנחנו רוצים שהפונקציה תחזיר.
חלקי ההגדרה של פונקציה אנונימית בעזרת מילת המפתח lambda
A girl has no name

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

נראה, לדוגמה, שימוש ב־filter כדי לסנן את כל האיברים שאינם חיוביים:


In [ ]:
def is_positive(number):
    return number > 0


numbers = [-2, -1, 0, 1, 2]
positive_numbers = filter(is_positive, numbers)
print(tuple(positive_numbers))

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


In [ ]:
numbers = [-2, -1, 0, 1, 2]
positive_numbers = filter(lambda n: n > 0, numbers)
print(tuple(positive_numbers))

איך זה עובד?
במקום להעביר ל־filter פונקציה שיצרנו מבעוד מועד, השתמשנו ב־lambda כדי ליצור פונקציה ממש באותה השורה.
הפונקציה שהגדרנו מקבלת מספר (n), ומחזירה True אם הוא חיובי, או False אחרת.
שימו לב שבצורה זו באמת לא היינו צריכים לתת שם לפונקציה שהגדרנו.

השימוש בפונקציות אנונימיות לא מוגבל ל־map ול־filter, כמובן.
מקובל להשתמש ב־lambda גם עבור פונקציות כמו sorted, שמקבלות פונקציה בתור ארגומנט.

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

נסדר, למשל, את הדמויות ברשימה הבאה, לפי תאריך הולדתן:


In [ ]:
closet = [
    {'name': 'Peter', 'year_of_birth': 1927, 'gender': 'Male'},
    {'name': 'Edmund', 'year_of_birth': 1930, 'gender': 'Male'},
    {'name': 'Lucy', 'year_of_birth': 1932, 'gender': 'Female'},
    {'name': 'Susan', 'year_of_birth': 1928, 'gender': 'Female'},
    {'name': 'Jadis', 'year_of_birth': 0, 'gender': 'Female'},
]

נרצה שסידור הרשימה יתבצע לפי המפתח year_of_birth.
כלומר, בהינתן מילון שמייצג דמות בשם d, יש להשיג את d['year_of_birth'], ולפיו לבצע את סידור הרשימה.
ניגש למלאכה:


In [ ]:
sorted(closet, key=lambda d: d['year_of_birth'])

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

סדרו את הדמויות ב־closet לפי האות האחרונה בשמם.

מונחים

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

תרגילים

פילטר מותאם אישית

כתבו פונקציה בשם my_filter שמתנהגת בדיוק כמו הפונקציה filter.
בפתירת התרגיל, המנעו משימוש ב־filter או במודולים.

נשאר? חיובי

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

ריצת 2,000

כתבו פונקציה בשם timer שמקבלת כפרמטר פונקציה (נקרא לה f) ופרמטרים נוספים.
הפונקציה timer תמדוד כמה זמן רצה פונקציה f כשמועברים אליה אותם פרמטרים.

לדוגמה:

  1. עבור הקריאה timer(print, "Hello"), תחזיר הפונקציה את משך זמן הביצוע של print("Hello").
  2. עבור הקריאה timer(zip, [1, 2, 3], [4, 5, 6]), תחזיר הפונקציה את משך זמן הביצוע של zip([1, 2, 3], [4, 5, 6]).
  3. עבור הקריאה timer("Hi {name}".format, name="Bug"), תחזיר הפונקציה את משך זמן הביצוע של "Hi {name}".format(name="Bug")