מתודות קסם#

טקסט המופיע למטה בסגול מציין קטעים המופיעים בסרטון

ניתן להיעזר בו כדי לחזור על התכנים או לעיין בהם שוב.

כבר מהשלבים הראשונים בלימוד פייתון ראינו שטיפוסים קיימים בפייתון כמו int, str, ו־list מתנהגים בצורה מאוד טבעית:
אנחנו יכולים להשתמש בהם באופרטורים (+, *, ==), להדפיס אותם ישירות עם print, לחשב את אורכם עם len או לבדוק הכלה בעזרת in.
למשל, הפקודה len([1,2,3]) מחזירה את אורך הרשימה.
טיפוסים הקיימים בפייתון עובדים בצורה טבעית עם פונקציות מובנות מבלי שנצטרך לחשוב מה מתרחש “מאחורי הקלעים”.

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

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

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

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

קיים מספר רב של מתודות קסם. ביחידה זו נלמד על שלוש מתודות קסם חדשות:

  • __repr__(self)

    • מחזירה מחרוזת שמייצגת את האובייקט.

    • משמשת בין השאר את הפונקציה print כדי לקבל מחרוזת שניתן להדפיס.

  • __len__(self)

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

    • מופעלת כאשר משתמשים בפונקציה המובנית len(obj).

  • __contains__(self, item)

    • קובעת האם איבר מסוים נמצא בתוך האובייקט.

    • מופעלת כאשר משתמשים באופרטור in (למשל: x in obj).

    • מחזירה True או False בהתאם לתוצאה.

בבעיה הבאה שנציג, נדגים את השימוש בשלוש המתודות המיוחדות הללו.

כדאי לדעת

גם __init__ (הבנאי) היא למעשה סוג של מתודת קסם מיוחדת, אשר נקראת בכל פעם שאנו קוראים למחלקה עם סוגריים עגולים

דוגמא: מימוש מבנה הנתונים LUD – Least Used Dictionary#

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

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

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

  • __init__(self, memory_size):

    • מאתחלת את המילון וקובעת את הגודל המקסימלי שלו ל־memory_size.

    • אם הערך של memory_size קטן מ־1, תודפס אזהרה: "Error in memory size".

  • __len__(self):

    • מחזירה את מספר האיברים שנמצאים כרגע במילון LUD.

  • __contains__(self, key):

    • מחזירה True אם המפתח נמצא במילון, אחרת False.

  • __repr__(self):

    • מחזירה מחרוזת המכילה את כל מפתחות המילון מופרדים ע”י מקף.

  • get(self, key):

    • מחזירה את הערך המתאים למפתח. בנוסף, הפעולה מעדכנת את מונה הגישות לאותו מפתח.

    • אם המפתח לא קיים במילון, המתודה מחזירה None.

  • put(self, key, value):

    • מוסיפה זוג חדש של מפתח וערך למילון.

הסבר על המימוש, שלב אחר שלב:#

הבנאי (__init__) מתבצע אתחול של האובייקט: אם המשתמש העביר גודל זיכרון קטן מ־1, תודפס הודעת שגיאה. לאחר מכן נשמר הערך של memory_size ומאותחלים שני מילונים פנימיים: data לשמירת המפתחות והערכים בפועל, ו-usage למעקב אחר מספר הפעמים שכל מפתח נשלף.

    def __init__(self, memory_size):
        if memory_size < 1:
            print('Error memory size')
        self.memory_size = memory_size
        self.data = {}
        self.usage = {}

המתודה __len__ מחזירה את מספר האיברים במילון, על ידי קריאה לפונקציה len על המילון data. בצורה זו יתאפשר לנו בהמשך להשתמש בפונקציה המובנית len ישירות על אובייקטים מטיפוס LUD.

    def __len__(self):
        return len(self.data) 

המתודה __contains__ מתבצעת בדיקת שייכות: האם מפתח מסוים מוכל במילון. זה יאפשר לנו בהמשך להשתמש בתחביר הטבעי key in lud במקום לקרוא לפונקציה נפרדת.

    def __contains__(self, key):
        return key in self.data

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

    def __repr__(self):
        return "-".join(self.data)

המתודה get אחראית להחזרת הערך השייך למפתח. היא משתמשת במתודת get של מילון רגיל, ואם הערך נמצא, מגדילה ב־1 את מונה הגישות לאותו מפתח במילון usage. אם המפתח לא קיים, מוחזר None.

    def get(self, key):
        val = self.data.get(key)
        if val != None:
            self.usage[key] += 1
        return val

המתודה put מוסיפה זוג מפתח-ערך חדש למילון ה-LUD.

    def put(self, key, value):
        if len(self) == self.memory_size and key not in self:
            mkey, mval = min(self.usage.items(), key=lambda key_value: key_value[1])
            self.data.pop(mkey)
            self.usage.pop(mkey)
        self.data[key] = value
        self.usage[key] = 0

נעקוב ביתר בפירוט אחר המימוש של פונקציה זו.
תחילה, מתבצעת בדיקה אם המילון כבר הגיע לגודל המקסימלי והמפתח החדש עדיין לא קיים:
if len(self) == self.memory_size and key not in self.
שימו לב כי הפקודות len וin מופעלות ישירות על self.
דבר זה מתאפשר בזכות המימוש שכתבנו למתודות הקסם שכתבנו: __len__ ו-__contains__.

במידה והמילון כבר הגיע למקסימום זוגות מפתח-ערך שהוא יכול להכיל (לפי memory_size), יש להסיר משני המילונים (data ו-usage) את המפתח שהשתמשו בו הכי מעט.
על מנת לגלות מי המפתח בו השתמשו הכי מעט נבצע את הפקודה:
mkey, mval = min(self.usage.items(), key=lambda key_value: key_value[1]).

נסביר פקודה זו מעט בהרחבה:
זכרו כי ()dict.items מחזירה סדרה של זוגות (key, value) — כלומר זוגות של מפתח-ערך.
אנו עושים שימוש בפרמטר key של הפונקציה min, אשר מגדיר יחס סדר חדש לפונקציה min.
יחס הסדר שהוגדר הוא לפי הערך המשויך לכל מפתח (כלומר, האיבר השני בכל tuple החוזר מdict.items()): lambda key_value: key_value[1], כלומר עבור כל זוג (key, count) נסתכל דווקא על האיבר השני בטאפל — מונה השימוש — ונבחר את המינימום על פי הערך הזה.
התוצאה של min היא הזוג מפתח-ערך עם הערך המינימלי שהוא בעצם מספר הפעמים שניגשו למפתח אליו ניגשו הכי פחות.
אם כך, נקבל את הזוג מפתח-ערך, ואנחנו מפרקים אותו ל־mkey (המפתח להסרה) ו־mval (מספר הגישות שלו).

לאחר שחילצנו את המפתח בעל מספר הגישות המינימלי, נסיר אותו משני המילונים שלנו (data ו-usage), ונוסיף את המפתח החדש יחד עם הערך המתאים. בנוסף, נאפס את מונה השימוש שלו במילון usage.

בצורה זו מובטחת התנהגות של Least Used Dictionary – בכל הוספה כשהמילון מלא, המפתח בעל השימוש המועט ביותר מוסר.

הרחבה

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

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

class LUD:
    def __init__(self, memory_size):
        if memory_size < 1:
            print('Error memory size')
        self.memory_size = memory_size
        self.data = {}
        self.usage = {}
        
    def __len__(self):
        return len(self.data) 

    def __contains__(self, key):
        return key in self.data

    def __repr__(self):
        return "-".join(self.data.keys())

    def get(self, key):
        val = self.data.get(key)
        if val != None:
            self.usage[key] += 1
        return val

    def put(self, key, value):
        if len(self) == self.memory_size and key not in self:
            mkey, mval = min(self.usage.items(), key=lambda key_value: key_value[1])
            self.data.pop(mkey)
            self.usage.pop(mkey)
        self.data[key] = value
        self.usage[key] = 0

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

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

lud_dict = LUD(2)
lud_dict.put('Hi', 2)
lud_dict.put('Bye', 2)
print(lud_dict)
a = lud_dict.get('Hi')
lud_dict.put('Hi again', 1)
print('Bye' in lud_dict)
print('Hi' in lud_dict)
print(lud_dict)
Hi-Bye
False
True
Hi-Hi again