לדלג לתוכן

הורשה בפייתון מוסברת - איך מחלקות באמת עוזרות זו לזו

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

מה זו בכלל הורשה - inheritance

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

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} משמיע קול"


class Dog(Animal):
    def bark(self):
        return f"{self.name} נובח"

שימו לב ש-Dog לא הגדיר בכלל init משלו, ועדיין אפשר לכתוב Dog("רקסי").speak() ולקבל תוצאה תקינה. הוא קיבל את זה בירושה מ-Animal.

זה בדיוק המבחן שקובע אם הורשה מתאימה - "כלב הוא חיה" זו טענה נכונה. זו נקראת יחס is-a, ואם אתם לא יכולים לומר את המשפט הזה בכנות על שתי המחלקות שלכם, כנראה שהורשה היא לא הכלי הנכון.

שיטת super - super().init()

בדוגמה למעלה Dog לא הגדיר init כי הוא לא היה צריך שום דבר מעבר למה שיש ל-Animal. אבל בדרך כלל למחלקת הילד יש שדות נוספים משלה, ואז צריך גם להריץ את הבנאי של האב וגם להוסיף את החלק החדש:

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

    def bark(self):
        return f"{self.name} מגזע {self.breed} נובח"

הקריאה ל-super().init(name) אומרת "תריץ קודם את הבנאי של המחלקה שממנה ירשתי, עם הפרמטרים האלה, ואז תמשיך משם". בלי זה, self.name בכלל לא יוגדר, ותקבלו שגיאה ברגע שתנסו להשתמש בו.

זה נכון גם למתודות רגילות, לא רק ל-init - אפשר לקרוא ל-super().speak() בתוך מתודה שדורסת אותה, כדי להריץ קודם את ההתנהגות המקורית ואז להוסיף עליה.

דריסת מתודות - method overriding

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

class Cat(Animal):
    def speak(self):
        return f"{self.name} מיילל"

עכשיו כשקוראים ל-speak() על מופע של Cat, פייתון מריץ את הגרסה שהוגדרה ב-Cat, לא את זו שב-Animal. זה בדיוק הכוח של פולימורפיזם - אפשר להריץ speak() על רשימה שמכילה גם כלבים וגם חתולים, בלי לדעת מראש איזה סוג חיה זה, וכל אחד יתנהג לפי המימוש שלו.

הרכבה לעומת הורשה - composition over inheritance

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

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

class Engine:
    def start(self):
        return "המנוע מותנע"


class Car:
    def __init__(self):
        self.engine = Engine()

    def start(self):
        return self.engine.start()

מכונית היא לא מנוע - זה יחס has-a, לא is-a. לכן כאן הרכבה הגיונית יותר מהורשה. הכלל הפשוט - אם אתם יכולים לתאר את הקשר כ"X הוא סוג של Y", הורשה מתאימה. אם הקשר הוא "X מכיל Y" או "X משתמש ב-Y", עדיף הרכבה. וברוב המקרים, שרשרת הורשה קצרה ורדודה עדיפה על אחת עמוקה ומסועפת.

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

לסיכום

הורשה היא כלי שימושי כשיש קשר אמיתי של is-a בין המחלקות, ו-super() הוא הדרך הנקייה לשלב בין ההתנהגות של האב לזו של הילד. אבל אל תתנו לזה להפוך להרגל אוטומטי - לפעמים הרכבה פשוטה תיתן לכם קוד נקי וגמיש הרבה יותר.

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