עד כה למדנו להכיר את עולמן של הפונקציות מקרוב:
במחברת זו נרכוש כלים נוספים שיאפשרו לנו גמישות רבה יותר בהגדרת פונקציות ובשימוש בהן.
כאשר אנחנו קוראים לפונקציה, יישלחו לפי הסדר הארגומנטים שנעביר אל הפרמטרים שמוגדרים בכותרת הפונקציה.
מצב כזה נקרא positional arguments ("ארגומנטים לפי מיקום").
נסתכל על פונקציה שמקבלת טווח (סוף והתחלה, בסדר הזה) ומחזירה רשימה של כל המספרים השלמים בטווח:
In [ ]:
def my_range(end, start):
numbers = []
i = start
while i < end:
numbers.append(i)
i += 1
return numbers
my_range(5, 0)
לפעמים נרצה לשנות את סדר הארגומנטים שאנחנו שולחים לפונקציה.
נעשה זאת בקריאה לפונקציה, על־ידי העברת שם הארגומנט ולאחר מכן העברת הערך שאנחנו רוצים להעביר אליו:
In [ ]:
my_range(start=0, end=5)
בשורה הזו הפכנו את סדר הארגומנטים.
כיוון שבקריאה כתבנו את שמות הפרמטרים התואמים לכותרת הפונקציה, הערכים נשלחו למקום הנכון.
השיטה הזו נקראת keyword arguments ("ארגומנטים לפי שם"), ובה אנחנו מעבירים את הארגומנטים שלנו לפי שמות הפרמטרים בכותרת הפונקציה.
אנחנו משתמשים בשיטה הזו אפילו כשאנחנו לא רוצים לשנות את סדר הארגומנטים, אלא רק לעשות קצת סדר בקוד.
נבחן, לדוגמה, את המקרה של הפונקציה random.randrange
– נעים יותר לראות קריאה לפונקציה עם שמות הפרמטרים:
In [ ]:
import random
In [ ]:
random.randrange(100, 200) # מובן פחות
In [ ]:
random.randrange(start=100, stop=200) # מובן יותר
למרות השימוש בסימן =
, לא מדובר פה בהשמה במובן הקלאסי שלה.
זוהי צורת כתיבה מיוחדת בקריאה לפונקציות שהמטרה שלה היא לסמן "העבר לפרמטר ששמו כך־וכך את הערך כך־וכך".
נזכר בפונקציה get
של מילון, שמאפשרת לקבל ממנו ערך לפי מפתח מסוים.
אם המפתח שאנחנו מחפשים לא קיים במילון, הפונקציה מחזירה None:
In [ ]:
ghibli_release_dates = {
'Castle in the Sky': '1986-08-02',
'My Neighbor Totoro': '1988-04-16',
'Spirited Away': '2001-07-20',
'Ponyo': '2008-07-19',
}
ponyo_release_date = ghibli_release_dates.get('Ponyo')
men_in_black_release_date = ghibli_release_dates.get('Men in Black')
print(f"Ponyo release date: {ponyo_release_date}")
print(f"Men in Black release date: {men_in_black_release_date}")
נממש את הפונקציה get
בעצמנו. לשם הנוחות, ייראה השימוש שונה במקצת:
In [ ]:
def get(dictionary, key):
if key in dictionary:
return dictionary[key]
return None
ponyo_release_date = get(ghibli_release_dates, 'Ponyo')
men_in_black_release_date = get(ghibli_release_dates, 'Men in Black')
print(f"Ponyo release date: {ponyo_release_date}")
print(f"Men in Black release date: {men_in_black_release_date}")
המימוש שלנו לא מושלם. הפעולה המקורית, get
על מילון, פועלת בצורה מתוחכמת יותר.
אפשר להעביר לה פרמטר נוסף, שקובע מה יחזור אם המפתח שהעברנו בפרמטר הראשון לא נמצא במילון:
In [ ]:
ponyo_release_date = ghibli_release_dates.get('Ponyo', '???')
men_in_black_release_date = ghibli_release_dates.get('Men in Black', '???')
print(f"Ponyo release date: {ponyo_release_date}")
print(f"Men in Black release date: {men_in_black_release_date}")
שימו לב להתנהגות המיוחדת של הפעולה get
!
אם המפתח שהעברנו בארגומנט הראשון לא קיים במילון, היא מחזירה את הערך שכתוב בארגומנט השני.
אפשר להעביר לה ארגומנט אחד, ואפשר להעביר לה שני ארגומנטים. היא מתפקדת כראוי בשני המצבים.
זו לא פעם ראשונה שאנחנו רואים פונקציות כאלו. למעשה, בשבוע שעבר למדנו על פעולות builtins רבות שמתנהגות כך:
range
, enumerate
ו־round
, כולן יודעות לקבל מספר משתנה של ארגומנטים.
נניח לפעולה get
בינתיים. אל דאגה, נחזור אליה בקרוב.
בזמן שאנחנו נחים מפעולות על מילונים יום האהבה מתקרב, וחנות הוורדים הקרובה מעוניינת להעלות את מחירי כל מוצריה בשקל אחד.
התבקשנו לבנות עבורם פונקציה שמקבלת רשימת מחירים, ומחזירה רשימה שבה כל איבר גדול ב־1 מרשימת המחירים המקורית.
ניגש לעבודה:
In [ ]:
def get_new_prices(l):
l2 = []
for item in l:
l2.append(item + 1)
return l2
prices = [42, 73, 300]
print(get_new_prices(prices))
בתוך זמן קצר הפונקציה שבנינו הופכת ללהיט היסטרי בחנויות הוורדים.
מנהל קרטל הוורדים הבין־לאומי ג'וזפה ורדי יוצר איתנו קשר, ומבקש לשכלל התוכנה כך שיוכל להעלות את מחירי המוצרים כרצונו.
כדי לעמוד בדרישה, נבנה פונקציה שמקבלת רשימה, ובנוסף אליה את המחיר שיתווסף לכל איבר ברשימה זו.
כך, אם הקורא לפונקציה יעביר כארגומנט השני את הערך 2, כל איבר ברשימה יגדל ב־2.
נממש בקלילות:
In [ ]:
def get_new_prices(l, increment_by):
l2 = []
for item in l:
l2.append(item + increment_by)
return l2
prices = [42, 73, 300]
print(get_new_prices(prices, 1))
print(get_new_prices(prices, 2))
ורדי פוצח בשירה מרוב אושר, ומבקש שכלול אחרון לפונקציה, אם אפשר.
אם הקורא לפונקציה העביר לה רק את רשימת המחירים, העלו את כל המחירים בשקל, כברירת מחדל.
אם כן הועבר הארגומנט השני, העלו את המחיר לפי הערך שצוין באותו ארגומנט.
הפעם אנחנו מתחבטים קצת יותר, מגרדים בראש, קוראים כמה מדריכי פייתון ומגיעים לבסוף לתשובה הבאה:
In [ ]:
def get_new_prices(l, increment_by=1):
l2 = []
for item in l:
l2.append(item + increment_by)
return l2
prices = [42, 73, 300]
print(prices)
print(get_new_prices(prices))
print(get_new_prices(prices, 5))
כשאנחנו רוצים להגדיר פרמטר עם ערך ברירת מחדל, נוכל לקבוע את ערך ברירת המחדל שלו בכותרת הפונקציה.
אם יועבר ארגומנט שכזה לפונקציה – פייתון תשתמש בערך שהועבר.
אם לא – יילקח ערך ברירת המחדל שהוגדר בכותרת הפונקציה.
במקרה שלנו הגדרנו את הפרמטר increment_by
עם ערך ברירת המחדל 1.
קריאה לפונקציה עם ארגומנט אחד בלבד (רשימת המחירים) תגדיל את כל המחירים ב־1, שהרי הוא ערך ברירת המחדל.
קריאה לפונקציה עם שני ארגומנטים (רשימת המחירים, סכום ההעלאה) תגדיל את כל המחירים בסכום ההעלאה שהועבר.
חשוב להבין שקריאה לפונקציה עם ערכים במקום ערכי ברירת המחדל, לא תשנה את ערך ברירת המחדל בקריאות הבאות:
In [ ]:
print(get_new_prices(prices, 5))
print(get_new_prices(prices))
ממשו את פונקציית get
המלאה. הפונקציה תקבל מילון, מפתח ו"ערך לשעת חירום".
החזירו את הערך השייך למפתח שהתקבל. אחרת – החזירו את הערך לשעת החירום שהועבר לפונקציה.
אם לא הועבר ערך לשעת חירום והמפתח לא נמצא במילון, החזירו None.
חשוב!
פתרו לפני שתמשיכו!
נדגים את אותו עיקרון עם כמה ערכי ברירת מחדל.
אם הדרישה הייתה, לדוגמה, להוסיף לפונקציה גם אפשרות להנחה במחירי הפרחים, היינו יכולים לממש זאת כך:
In [ ]:
def get_new_prices(l, increment_by=1, discount=0):
l2 = []
for item in l:
new_price = item + increment_by - discount
l2.append(new_price)
return l2
prices = [42, 73, 300]
print(prices)
print(get_new_prices(prices, 10, 1)) # העלאה של 10, הנחה של 1
print(get_new_prices(prices, 5)) # העלאה של 5
אך מה יקרה כשנרצה לתת רק הנחה?
במקרה כזה, כשנרצה "לדלג" מעל אחד מערכי ברירת המחדל, נצטרך להעביר את שמות הפרמטרים בקריאה לפונקציה.
בדוגמה הבאה אנחנו מעלים את המחיר ב־1 (כי זו ברירת המחדל), ומורידים אותו ב־5:
In [ ]:
prices = [42, 73, 300]
print(prices)
print(get_new_prices(prices, discount=5))
זה אמנם עניין של סגנון, אבל יש יופי וסדר בציון שמות הפרמטרים גם כשלא חייבים:
In [ ]:
print(get_new_prices(prices, increment_by=10, discount=1))
הפונקציה הפייתונית max
, למשל, מתנהגת באופן משונה.
היא יודעת לקבל כל מספר שהוא של ארגומנטים, ולהחליט מי מהם הוא הגדול ביותר.
ראו בעצמכם!
In [ ]:
max(13, 256, 278, 887, 989, 457, 6510, 18, 865, 901, 401, 704, 640)
נוכל גם אנחנו לממש פונקציה שמקבלת מספר משתנה של פרמטרים די בקלות.
נתחיל מלממש פונקציה טיפשית למדי, שמקבלת מספר משתנה של פרמטרים ומדפיסה אותם:
In [ ]:
def silly_function(*parameters):
print(parameters)
print(type(parameters))
print('-' * 20)
silly_function('Shmulik', 'Shlomo')
silly_function('Shmulik', 1, 1, 2, 3, 5, 8, 13)
silly_function()
מה התרחש בדוגמה האחרונה, בעצם?
כשפרמטר מוגדר בכותרת הפונקציה עם הסימן כוכבית, אפשר לשלוח אל אותו פרמטר מספר בלתי מוגבל של ארגומנטים.
הערך שייכנס לפרמטר יהיה מסוג tuple
, שאיבריו הם כל האיברים שהועברו כארגומנטים.
לצורך ההדגמה, נבנה פונקציה שמקבלת פרמטרים ומדפיסה אותם בזה אחר זה:
In [ ]:
def silly_function2(*parameters):
print(f"Printing all the items in {parameters}:")
for parameter in parameters:
print(parameter)
print("-" * 20)
silly_function2('Shmulik', 'Shlomo')
silly_function2('Shmulik', 1, 1, 2, 3, 5, 8, 13)
silly_function2()
שחקו עם הפונקציה silly_function2
וודאו שהבנתם מה מתרחש בה.
כשתסיימו, נסו לממש את הפונקציה max
בעצמכם.
חשוב!
פתרו לפני שתמשיכו!
נממש את max
:
In [ ]:
def my_max(*numbers):
if not numbers: # אם לא סופקו ארגומנטים, אין מקסימום
return None
maximum = numbers[0]
for number in numbers:
if number > maximum:
maximum = number
return maximum
my_max(13, 256, 278, 887, 989, 457, 6510, 18, 865, 901, 401, 704, 640)
כותרת הפונקציה יכולה לכלול משתנים נוספים לפני הכוכבית.
נראה לדוגמה פונקציה שמקבלת גובה הנחה ואת מחירי כל המוצרים שקנינו, ומחזירה את הסכום הסופי שעלינו לשלם:
In [ ]:
def get_final_price(discount, *prices):
return sum(prices) - discount
get_final_price(10000, 3.141, 90053)
אף שבמבט ראשון הפונקציה get_final_price
עשויה להיראות מגניבה, כדאי להיזהר משימוש מוגזם בתכונה הזו של פייתון.
הדוגמה הזו אמנם מדגימה גמישות יוצאת דופן של פייתון, אבל ככלל היא דוגמה גרועה מאוד לשימוש בכוכבית.
שימו לב כמה נוח יותר להבין את המימוש הבא ל־get_final_price
, וכמה נוח יותר להבין את הקריאה לפונקציה הזו:
In [ ]:
def get_final_price(prices, discount):
return sum(prices) - discount
get_final_price(prices=(3.141, 90053), discount=10000)
כתבו פונקציה בשם create_path
שיכולה לקבל מספר בלתי מוגבל של ארגומנטים.
הפרמטר הראשון יהיה אות הכונן שבו הקבצים מאוחסנים (לרוב "C"), והפרמטרים שאחריו יהיו שמות של תיקיות וקבצים.
שרשרו אותם בעזרת התו \
כדי ליצור מהם מחרוזת המייצגת נתיב במחשב. אחרי האות של הכונן שימו נקודתיים.
הניחו שהקלט שהמשתמש הכניס הוא תקין.
הנה כמה דוגמאות לקריאות לפונקציה ולערכי ההחזרה שלה:
create_path("C", "Users", "Yam")
תחזיר "C:\Users\Yam"create_path("C", "Users", "Yam", "HaimonLimon.mp4")
תחזיר "C:\Users\Yam\HaimonLimon.mp4"create_path("D", "1337.png")
תחזיר "D:\1337.png"create_path("C")
תחזיר "C:"create_path()
תגרום לשגיאהבתחילת המחברת למדנו כיצד מעבירים לפונקציות ארגומנטים בעזרת שם:
In [ ]:
def print_introduction(name, age):
return f"My name is {name} and I am {age} years old."
print_introduction(age=2019, name="Gandalf")
אבל מה אם נרצה להעביר לפונקציה שלנו מספר בלתי מוגבל של ארגומנטים לפי שם?
נביא כדוגמה את הפעולה format
על מחרוזות.
format
היא פונקציה גמישה בכל הנוגע לכמות ולשמות של הארגומנטים שמועברים לה לפי שם.
נראה שתי דוגמאות לשימוש בה, שימוש שבמבט ראשון עשוי להיראות קסום:
In [ ]:
message = "My name is {name} and I am {age} years old"
formatted_message = message.format(name="Gandalf", age=2019)
print(formatted_message)
In [ ]:
song = "I'll {action} a story of a {animal}.\nA {animal} who's {key} is {value}."
formatted_song = song.format(action="sing", animal="duck", key="name", value="Alfred Kwak")
print(formatted_song)
נכתוב גם אנחנו פונקציה שמסוגלת לקבל מספר בלתי מוגבל של ארגומנטים לפי שם.
ניעזר תחילה בידידתנו הוותיקה, silly_function
, כדי לראות איך הקסם קורה:
In [ ]:
def silly_function(**kwargs):
print(kwargs)
print(type(kwargs))
silly_function(a=5, b=6, address="221B Baker St, London, England.")
ההתנהגות הזו מתרחשת כיוון שהשתמשנו בשתי כוכביות לפני שם המשתנה.
השימוש בשתי כוכביות מאפשר לנו להעביר מספר בלתי מוגבל של ארגומנטים עם שם, באופן שמזכיר קצת את השימוש בכוכבית שראינו קודם.
המשתנה שבו נשמרים הנתונים הוא מסוג מילון, ובו המפתחות יהיו שמות הארגומנטים שהועברו, והערכים – הערכים שהועברו לאותם שמות.
אחרי שהבנו איך הסיפור הזה עובד, בואו ננסה ליצור פונקציה מעניינת יותר.
הפונקציה שנכתוב תקבל כארגומנטים כמה גרם מכל רכיב צריך כדי להכין סושי, ותדפיס לנו מתכון:
In [ ]:
def print_sushi_recipe(**ingredients_and_amounts):
for ingredient, amount in ingredients_and_amounts.items():
print(f"{amount} grams of {ingredient}")
print_sushi_recipe(rice=300, water=300, vinegar=15, sugar=10, salt=3, fish=600)
בדוגמה זו השתמשנו בעובדה שהפרמטר שמוגדר בעזרת שתי כוכביות הוא בהכרח מילון.
עברנו על כל המפתחות והערכים שבו בעזרת הפעולה items
, והדפסנו את המתכון, רכיב אחר רכיב.
גרמו לפונקציה print_sushi_recipe
להדפיס את הרכיבים לפי סדר משקלם, מהנמוך לגבוה.
פרמטר המוגדר בעזרת שתי כוכביות תמיד יופיע בסוף רשימת הפרמטרים.
כתבו פונקציה בשם my_format
שמקבלת מחרוזת, ומספר בלתי מוגבל של פרמטרים עם שמות.
הפונקציה תחליף כל הופעה של {key}
במחרוזת, אם key
הועבר כפרמטר לפונקציה.
הערך שבו {key}
יוחלף הוא הערך שהועבר ל־key
במסגרת העברת הארגומנטים לפונקציה.
הפונקציה לא תשתמש בפעולה format
של מחרוזות או בפונקציות שלא למדנו עד כה.
הנה כמה דוגמאות לקריאות לפונקציה ולערכי ההחזרה שלה:
my_format("I'm Mr. {name}, look at me!", name="Meeseeks")
my_format("{a} {b} {c} {c}", a="wubba", b="lubba", c="dub")
my_format("The universe is basically an animal", animal="Chicken")
my_format("The universe is basically an animal")
נוכל לשלב יחד את כל הטכניקות שלמדנו עד עכשיו לפונקציה אחת.
ניצור, לדוגמה, פונקציה שמחשבת עלות הכנה של עוגה.
הפונקציה תקבל:
לצורך פישוט התרגיל, נתעלם לרגע מעניין הכמויות במתכון :)
In [ ]:
def calculate_cake_price(apply_discount, *ingredients, discount_rate=10, **prices):
if not apply_discount:
discount_rate = 0
final_price = 0
for ingredient in ingredients:
final_price = final_price + prices.get(ingredient)
final_price = final_price - (final_price * discount_rate / 100)
return final_price
calculate_cake_price(True, 'chocolate', 'cream', chocolate=30, cream=20, water=5)
הפונקציה נכתבה כדי להדגים את הטכניקה, והיא נראית די רע.
ראו כמה קשה להבין איזה ארגומנט שייך לאיזה פרמטר בקריאה לפונקציה.
יש להפעיל שיקול דעת לפני שימוש בטכניקות של קבלת פרמטרים מרובים.
שימו לב לסדר הפרמטרים בכותרת הפונקציה:
apply_discount
).ingredients
).discount_rate
).prices
).נסו לחשוב: למה נקבע דווקא הסדר הזה?
איך הייתם כותבים את אותה הפונקציה בדיוק בלי שימוש בטכניקות שלמדנו?
השימוש בערכי ברירת מחדל מותר.
חשוב!
פתרו לפני שתמשיכו!
יש מקרה קצה של ערכי ברירת מחדל שגורם לפייתון להתנהג קצת מוזר.
זה קורה כשערך ברירת המחדל שהוגדר בכותרת הפונקציה הוא mutable:
In [ ]:
def append(item, l=[]):
l.append(item)
return l
print(append(4, [1, 2, 3]))
print(append('a'))
עד כאן נראה כאילו הפונקציה פועלת באופן שהיינו מצפים ממנה.
ערך ברירת המחדל של הפרמטר l
הוא רשימה ריקה, ולכן בקריאה השנייה חוזרת רשימה עם איבר בודד, ['a'].
נקרא לפונקציה עוד כמה פעמים, ונגלה משהו מוזר:
In [ ]:
print(append('b'))
print(append('c'))
print(append('d'))
print(append(4, [1, 2, 3]))
print(append('e'))
משונה ולא הגיוני! ציפינו לקבל את הרשימה ['b'] ואז את הרשימה ['c'] וכן הלאה.
במקום זה בכל פעם מצטרף איבר חדש לרשימה. למה?
הסיבה לכך היא שפייתון קוראת את כותרת הפונקציה רק פעם אחת – בשלב ההגדרה של הפונקציה.
בשלב הזה שבו פייתון תקרא את כותרת הפונקציה, ערך ברירת המחדל של l
יצביע לרשימה ריקה.
מאותו רגע, בכל פעם שלא נעביר ל־l
ערך, l
תהיה אותה רשימת ברירת מחדל שהגדרנו בהתחלה.
נדגים זאת בעזרת הדפסת ה־id
של הרשימה:
In [ ]:
def view_memory_of_l(item, l=[]):
l.append(item)
print(f"{l} --> {id(l)}")
return l
same_list1 = view_memory_of_l('a')
same_list2 = view_memory_of_l('b')
same_list3 = view_memory_of_l('c')
new_list1 = view_memory_of_l(4, [1, 2, 3])
new_list2 = view_memory_of_l(5, [1, 2, 3])
new_list3 = view_memory_of_l(6, [1, 2, 3])
כיצד נפתור את הבעיה?
דבר ראשון – נשתדל שלא להגדיר משתנים מטיפוס שהוא mutable בתוך כותרת הפונקציה.
אם נרצה בכל זאת שהפרמטר יקבל רשימה כברירת מחדל, נעשה זאת כך:
In [ ]:
def append(item, l=None):
if l == None:
l = []
l.append(item)
return l
print(append(4, [1, 2, 3]))
print(append('a'))
שימו לב שהתופעה לא משתחזרת במבנים שהם immutable, כיוון שכשמם כן הם – אי אפשר לשנותם:
In [ ]:
def increment(i=0):
i = i + 1
return i
print(increment(100))
print(increment())
print(increment())
print(increment())
print(increment(100))
נרענן את זיכרוננו בנוגע ל־unpacking:
In [ ]:
range_arguments = [1, 10, 3]
range_result = range(*range_arguments)
print(list(range_result))
או:
In [ ]:
preformatted_message = "My name is {me}, and my sister is {sister}"
parameters = {'me': 'Mei', 'sister': 'Satsuki'}
message = preformatted_message.format(**parameters)
print(message)
אם כך, נוכל לכתוב:
In [ ]:
def get(dictionary, *args, **kwargs):
return dictionary.get(*args, **kwargs)
שימו לב שהכוכביות בשורה הראשונה עוזרות לנו לקבל מספר משתנה של ארגומנטים.
הכוכביות בשורה השנייה הן unpacking, כפי שלמדנו בשבוע שעבר.
החיקוי הזה לא מועיל לנו במיוחד כרגע, אבל הוא יעבוד עבור כל סוג של פעולה.
*args
. מאפשר לנו לקבל מספר בלתי מוגבל של ארגומנטים לפי מיקום.**kwargs
. מאפשר לנו לקבל מספר בלתי מוגבל של ארגומנטים לפי שם.כתבו פונקציה בשם avg שמקבלת מספר בלתי מוגבל של ארגומנטים, ומדפיסה את הממוצע שלהם.
avg(5, 6)
תחזיר 5.5
avg(10, 5, 3)
תחזיר 6
avg(2)
תחזיר 2
avg()
תחזיר None או שגיאה, לבחירתכם
כתבו פונקציה בשם join, שמקבלת מספר בלתי מוגבל של רשימות, כל רשימה כפרמטר.
על הפונקציה להיות מסוגלת לקבל פרמטר נוסף בשם sep
.
על הפונקציה להחזיר רשימה אחת המורכבת מכלל הרשימות שהתקבלו כפרמטרים.
אם סופק הפרמטר sep, יש לשרשר אותו כאיבר בין כל שתי רשימות. אם הוא לא סופק, יש לשרשר את התו "-"
במקום.
join([1, 2], [8], [9, 5, 6], sep='@')
תחזיר [1, 2, '@', 8, '@', 9, 5, 6]
join([1, 2], [8], [9, 5, 6])
תחזיר [1, 2, '-', 8, '-', 9, 5, 6]
join([1])
תחזיר [1]
join()
תחזיר None או שגיאה, לבחירתכם
ממשו פונקציה בשם get_recipe_price, שלה יש:
הפונקציה תחזיר את המחיר שעלינו לשלם על קניית כל המצרכים.
get_recipe_price({'chocolate': 18, 'milk': 8}, chocolate=200, milk=100)
get_recipe_price({'chocolate': 18, 'milk': 8}, optionals=['milk'], chocolate=300)
get_recipe_price({})