בפסקאות הקרובות נבחן פונקציות מזווית ראייה מעט שונה מהרגיל.
בואו נקפוץ ישירות למים!
תכונה מעניינת שמתקיימת בפייתון היא שפונקציה היא ערך, בדיוק כמו כל ערך אחר.
נגדיר פונקציה שמעלה מספר בריבוע:
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 מקבלת פונקציה כפרמטר הראשון, ו־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 מקבלת פונקציה כפרמטר ראשון, ו־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))
לפני שנסביר איפה החלק של ה"פונקציה בלי שם" נתמקד בצד ימין של ההשמה.
כיצד מנוסחת הגדרת פונקציה אנונימית?
lambda
.
במה שונה ההגדרה של פונקציה זו מההגדרה של פונקציה רגילה?
היא לא באמת שונה.
המטרה היא לאפשר תחביר שיקל על חיינו כשאנחנו רוצים לכתוב פונקציה קצרצרה שאורכה שורה אחת.
נראה, לדוגמה, שימוש ב־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
.
אפשר להניח שהקלט מהמשתמש תקין.
כתבו פונקציה בשם timer שמקבלת כפרמטר פונקציה (נקרא לה f) ופרמטרים נוספים.
הפונקציה timer תמדוד כמה זמן רצה פונקציה f כשמועברים אליה אותם פרמטרים.
לדוגמה:
timer(print, "Hello")
, תחזיר הפונקציה את משך זמן הביצוע של print("Hello")
.timer(zip, [1, 2, 3], [4, 5, 6])
, תחזיר הפונקציה את משך זמן הביצוע של zip([1, 2, 3], [4, 5, 6])
.timer("Hi {name}".format, name="Bug")
, תחזיר הפונקציה את משך זמן הביצוע של "Hi {name}".format(name="Bug")