לדלג לתוכן

מתודות קסם בפייתון - dunder methods

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

למה בכלל צריך את זה

תחשבו על זה ככה - כשאתם כותבים len(my_list) או my_list[0] או "a" + "b", פייתון לא באמת יודע מראש איך כל טיפוס אמור להתנהג עם len או עם חיבור. במקום זה, כל טיפוס מגדיר בעצמו איך הוא מגיב לפעולות האלה, דרך מתודות עם שמות קבועים שפייתון קורא להן מאחורי הקלעים. כשאתם כותבים len(x), פייתון בעצם קורא ל-x.__len__(). כשאתם כותבים x + y, פייתון קורא ל-x.__add__(y). ברגע שאתם ממשים את המתודות האלה במחלקה שלכם, האובייקטים שלכם מתחילים להתנהג בדיוק כמו רשימה, מחרוזת או מספר - טבעי לגמרי בתוך שאר הקוד.

המתודה שכולם מכירים - init

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

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

ההבדל בין str ל-repr

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

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

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

    def __str__(self):
        return f"({self.x}, {self.y})"

p = Point(3, 4)
print(p)       # (3, 4)
print(repr(p)) # Point(x=3, y=4)

הכלל הפרקטי - אם הגדרתם רק __repr__ ולא __str__, פייתון ישתמש ב-__repr__ גם כברירת מחדל ל-print, כי הוא לא חייב שיהיו שתי המתודות. אבל אם אתם רוצים פלט נוח לקריאה בנוסף לפלט טכני לדיבוג, כדאי להגדיר את שתיהן בנפרד.

השוואה ואורך - eq ו-len

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

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

print(Point(1, 2) == Point(1, 2))  # True

באותה צורה, __len__ מאפשר להשתמש בפונקציה len() על אובייקטים מותאמים אישית, למשל מחלקה שמייצגת אוסף כלשהו.

דוגמה מלאה - מחלקת Money

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

class Money:
    def __init__(self, amount):
        self.amount = amount

    def __repr__(self):
        return f"Money({self.amount})"

    def __add__(self, other):
        return Money(self.amount + other.amount)

    def __eq__(self, other):
        return self.amount == other.amount

wallet = Money(50) + Money(30)
print(wallet)  # Money(80)

בזכות __add__ אפשר לכתוב Money(50) + Money(30) כמו שהייתם מחברים שני מספרים רגילים, בלי לקרוא לפונקציה מסורבלת בשם add_money. זה בדיוק הכוח של dunder methods - הן נותנות למחלקות שלכם לדבר באותה שפה כמו הטיפוסים המובנים של פייתון.

מתי כדאי להשתמש בהן

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

לסיכום

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

הצטרפו לקהילה בדיסקורד