Unpacking

הקדמה

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


In [ ]:
country, population = ('Israel', 8712000)

בצד ימין יש tuple שמכיל שני איברים, ואנחנו מבצעים השמה לשני משתנים ששמותיהם country ו־population.
אומנם לא הייתי מעניק לפיסת הקוד הזו את תואר פיסת הקוד הנאה בעולם, אבל נראה שפייתון יודעת להריץ אותה:


In [ ]:
print(f"There are {population} people living in {country}.")

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

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

Unpacking לתוך משתנים

המקרה הקלאסי

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


In [ ]:
a, b, c, d, e = (1, 2, 3, 4, 5)

הטכניקה הזו עובדת גם עבור טיפוסים אחרים שמוגדרים כ־iterable:


In [ ]:
a, b = [1, 2]

ואפילו בלי סוגריים (זה לא קסם – בצד ימין נוצר בפועל tuple):


In [ ]:
a, b = 1, 2

או כ"חילוץ איברים" מתוך משתנה קיים לתוך כמה משתנים נפרדים:


In [ ]:
point_on_map = (36.672011, 65.807761)
x, y = point_on_map
print(f"The treasure should be in ({x}, {y}).")

בדוגמה האחרונה יצרנו בשורה הראשונה משתנה שמצביע ל־tuple. ב־tuple ישנם שני מספרים מסוג float.
בשורה השנייה פירקנו את ה־tuple – הערך הראשון שלו הוכנס לתוך המשתנה x והערך השני שלו הוכנס לתוך המשתנה y.

unpacking בלולאות for

שימוש מקובל מאוד ל־unpacking, שאותו כבר הספקתם לראות בעבר, מתרחש בלולאות for.
ניצור רשימה של tuple־ים, שבה כל tuple ייצג מדינה ואת מספר האנשים החיים בה:


In [ ]:
countries_with_population = [
    ('Cyprus', 1198575),
    ('Eswatini', 1148130),
    ('Djibouti', 973560),
    ('Fiji', 889953),
]

בעולם ללא unpacking, היינו צריכים לכתוב כך:


In [ ]:
for item in countries_with_population:
    country = item[0]
    population = item[1]
    print(f"There are {population:9,} people in {country}.")

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


In [ ]:
for country, population in countries_with_population:
    print(f"There are {population:9,} people in {country}.")
תצוגה של המשתנה countries_with_population ושל צורת הפירוק שלו
0 1 2 3
0 1
"Cyprus" 1198575
-2 -1
0 1
"Eswatini" 1148130
-2 -1
0 1
"Djibouti" 973560
-2 -1
0 1
"Fiji" 889953
-2 -1
-4 -3 -2 -1

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

במילונים

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


In [ ]:
countries_with_population = {
    'Cyprus': 1198575,
    'Eswatini': 1148130,
    'Djibouti': 973560,
    'Fiji': 889953,
}

In [ ]:
for country, population in countries_with_population.items():
    print(f"There are {population:9,} people in {country}.")

unpacking לערכי חזרה מפונקציה

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


In [ ]:
def division_and_modulo(number, divisor):
    division = number // divisor
    modulo = number % divisor
    return (division, modulo)  # אפשר גם בלי הסוגריים

נשתמש בפונקציה:


In [ ]:
division_and_modulo(5, 2)

והרי שאם מוחזר לנו tuple, אפשר לעשות לו unpacking:


In [ ]:
div, mod = division_and_modulo(5, 2)
print(f"division: {div}, modulo: {mod}")

Unpacking לארגומנטים

נבחן את הקוד הפשוט הבא:


In [ ]:
def print_treasure_location(x, y):
    print(f"{x}°N, {y}°E")

treasure_location = (36.671111, 65.808056)
print_treasure_location(treasure_location[0], treasure_location[1])

הגדרנו פונקציה שמדפיסה לנו יפה מיקומים לפי x ו־y שהיא מקבלת.
המימוש הגיוני, אבל אוי א בראך! השימוש לא מאוד נוח!
בכל פעם אנחנו צריכים לפרק את ה־tuple שמכיל את המיקום ל־2 איברים, ולשלוח כל אחד מהם בנפרד.

unpacking לארגומנטים לפי מיקום

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


In [ ]:
def print_treasure_location(x, y):
    print(f"{x}°N, {y}°E")

treasure_location = (36.671111, 65.808056)
print_treasure_location(*treasure_location)

נראה מוזר? זו לא טעות, זהו באמת תחביר שעדיין לא ראינו!
הכוכבית מפרקת את ה־tuple שהגדרנו, treasure_location, ושולחת לארגומנט x את הערך הראשון ולארגומנט y את הערך השני.

אם היו לנו ערכים רבים, היינו יכולים להשתמש באותו טריק בלולאה:


In [ ]:
treasure_locations = [
    (36.671111, 65.808056),
    (53.759748, -2.648121),
    (52.333333, 1.183333),
    (52.655278, -1.906667),
]

In [ ]:
def print_treasure_location(x, y):
    print(f"{x}°N, {y}°E")

for treasure_location in treasure_locations:
    print_treasure_location(*treasure_location)

אבל נזכור שאנחנו יכולים להשתמש גם בתעלול שלמדנו על unpacking בתוך for:


In [ ]:
def print_treasure_location(x, y):
    print(f"{x}°N, {y}°E")

for treasure_x, treasure_y in treasure_locations:
    print_treasure_location(treasure_x, treasure_y)

unpacking לארגומנטים לפי שם הארגומנט

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


In [ ]:
treasure_maps = [
    {'x': 36.671111, 'y': 65.808056},
    {'x': 53.759748, 'y': -2.648121},
    {'x': 52.333333, 'y': 1.183333},
    {'x': 52.655278, 'y': -1.906667}
]

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


In [ ]:
def print_treasure_location(x, y):
    print(f"{x}°N, {y}°E")

for location in treasure_maps:
    print_treasure_location(location.get('x'), location.get('y'))

או שאולי לא?

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


In [ ]:
treasure_maps = [
    {'x': 36.671111, 'y': 65.808056},
    {'x': 53.759748, 'y': -2.648121},
    {'x': 52.333333, 'y': 1.183333},
    {'x': 52.655278, 'y': -1.906667}
]

def print_treasure_location(x, y):
    print(f"{x}°N, {y}°E")

for location in treasure_maps:
    print_treasure_location(**location)

מה קרה פה?
באיטרציה הראשונה location היה {'x': 36.671111, 'y': 65.808056}.
הפונקציה print_treasure_locations מחכה שיעבירו לה ערך לפרמטר x ולפרמטר y.
ה־unpacking שעשינו בעזרת שתי הכוכביות העביר את הערך של המפתח 'x' במילון לפרמטר x, ואת הערך של המפתח 'y' במילון לפרמטר y.

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


In [ ]:
def stringify_date(year, month, day):
    return f'{year}-{month}-{day}'

date = {'year': 1815, 'month': 12, 'day': 10}
print(stringify_date(**date))

שגיאות

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


In [ ]:
a, b, c = (1, 2)

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


In [ ]:
def print_treasure_location(x, y):
    print(f"{x}°N, {y}°E")

location_3d = (36.671111, 65.808056, 63.124592)
print_treasure_location(*location_3d)

אם ננסה לעשות unpacking לאיבר שאינו iterable, תתקבל השגיאה הבאה:


In [ ]:
a, b = 5

קוד לדוגמה

סדרת פיבונאצ'י היא סדרה שמתחילה באיברים 1 ו־1, וכל איבר בה הוא סכום שני האיברים הקודמים לו.
האיברים הראשונים בסדרה, הם (מימין לשמאל) 1, 1, 2, 3, 5, 8, וכך הסדרה ממשיכה.
במימוש פונקציה שמקבלת מספר ומחזירה את סכום כל איברי הסדרה עד אותו מספר, נוכל להשתמש ב־unpacking כדי לשפר את הקריאות של הפונקציה:

הפונקציה בלי unpacking:


In [ ]:
def fibonacci_no_unpacking(number):
    a = 1
    b = 1
    total = 0
    while a <= number:
        total = total + a
        temp = a
        a = b
        b = temp + b
    return total

הפונקציה עם unpacking:


In [ ]:
def fibonacci_sum(number):
    a, b = 1, 1
    total = 0
    while a <= number:
        total = total + a
        a, b = b, a + b
    return total

fibonacci_sum(8)

תרגילים

אליבי לרוצחים

לפניכם tuple המכיל כמה מילונים, כאשר כל מילון מייצג דמות חשודה ברצח.
בתוך כל אחד מהמילונים, תחת המפתח evidences, ישנו tuple שבו שני איברים.
האיבר הראשון הוא הנשק שתפסה המשטרה, והאיבר השני הוא המיקום המרכזי שבו הייתה הדמות באותו היום.
בהינתן שהרוצח השתמש באקדח דרינגר (derringer) ב־Petersen House, הדפיסו רק את שמות האנשים שעדיין חשודים ברצח.
השתדלו להשתמש ב־unpacking לפחות פעמיים.


In [5]:
suspects = (
  {'name': 'Anne', 'evidences': ('derringer', 'Caesarea')},
  {'name': 'Taotao', 'evidences': ('derringer', 'Petersen House')},
  {'name': 'Pilpelet', 'evidences': ('Master Sword', 'Hyrule')},
)

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


In [ ]:
def check_evidences(weapon, location):
    return weapon.lower() == 'derringer' and location.lower() == 'petersen house'