מתודות קסם בפייתון - 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 הכי מוכרת, נקראת אוטומטית בכל פעם שיוצרים אובייקט חדש מהמחלקה, ואחראית לאתחל את התכונות ההתחלתיות שלו.
ההבדל בין 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 הן לא תוספת קישוטית, הן הדרך שבה פייתון מאפשר לכם לבנות טיפוסים שמתנהגים באופן טבעי לגמרי בתוך שאר השפה. ברגע שתתחילו לשים לב אליהן, תגלו אותן כמעט בכל מקום - הן פשוט חלק מהשפה שנשאר בדרך כלל בלתי נראה.