2.22. הורשה

הורשה מאפשרת למחלקה להרחיב מחלקה אחרת – להתחיל עם כל המתודות והתכונות שלה, ואז להוסיף או לדרוס את מה ששונה. המקורית נקראת הבסיס (או ההורה); ההרחבה נקראת התת-מחלקה.

A base class Shape pointing down to a subclass Square; Square overrides one method (area) and inherits the rest.

Square משתמשת מחדש בכל מתודה ב-Shape ודורסת את אלה שהיא צריכה להתמחות בהן.

2.22.1. הגדרת תת-מחלקה

מחלקת הבסיס נכנסת לסוגריים שבשורת ה-class:

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

    def describe(self):
        return self.name + " with area " + str(self.area())

    def area(self):
        return 0

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")
        self.side = side

    def area(self):
        return self.side * self.side

s = Square(4)
print(s.describe())

פלט:

square with area 16

Square יורשת את describe מ-Shape ללא שינוי ודורסת את area כדי להחזיר ערך אמיתי. ה-describe של הבסיס עדיין עובד מכיוון שהוא קורא ל-self.area() – אשר נפתר לדריסה בזמן ריצה.

2.22.2. super()

super() מחזיר proxy המאפשר למתודה לקרוא לגרסת מחלקת-הבסיס של מתודה. השימוש הנפוץ ביותר הוא קריאה ל-__init__ של הבסיס מתוך __init__ של תת-מחלקה כך שהבסיס יקבל הזדמנות לאתחל את התכונות שלו עצמו:

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")    # run Shape.__init__
        self.side = side

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

2.22.2.1. מחלקת הבסיס המשתמעת

כל מחלקה יורשת באופן משתמע מ-object, השורש של היררכיית הטיפוסים של Python. מתודות כמו __repr__, __eq__, והמנגנון שמאחורי גישה לתכונות – כולם מגיעים משם גם כאשר מחלקה אינה מצהירה על בסיס. כתיבת class Foo(object): במפורש וכתיבת class Foo: שקולות ב-Python מודרני; האחרונה היא הצורה המקובלת.

2.22.3. מתי הורשה עוזרת – ומתי לא

השתמשו בהורשה כאשר מחלקה אחת היא באמת סוג ספציפי יותר של אחרת, החולקת את רוב ההתנהגות. המבחן הקלאסי הוא בדיקת ה-”is-a“: Square הוא Shape.

כאשר שתי מחלקות פשוט במקרה חולקות כמה עוזרים, הורשה היא מוגזמת. פנו לקומפוזיציה במקום: גרמו למחלקה אחת להחזיק מופע של האחרת כתכונה ולהשתמש בה דרך אותה תכונה. הצורה היא ”has-a“, לא ”is-a“:

class Logger:
    def log(self, msg):
        print("[log]", msg)

# inheritance: Worker IS-A Logger
class WorkerInherits(Logger):
    def run(self):
        self.log("starting")

# composition: Worker HAS-A Logger
class WorkerComposes:
    def __init__(self):
        self.logger = Logger()

    def run(self):
        self.logger.log("starting")

שתיהן עובדות. גרסת הקומפוזיציה בדרך כלל טובה יותר מכיוון שהיא:

  • שומרת על ממשקים קטנים. WorkerComposes חושפת רק run ו-logger. WorkerInherits חושפת גם log – קוראים יכולים לכתוב worker.log(...) ישירות, בין אם זה היה מכוון ובין אם לא.

  • מנתקת מחזורי חיים. ניתן להחליף את ה-logger, לחלוק אותו בין worker-ים, או לבנות אותו אחרת לכל worker, הכל מבלי לגעת במחלקה Worker. תת-מחלקה מהודקת לבסיס שלה.

  • נמנעת מהתנגשויות שמות-מתודות. שני בסיסים שכל אחד מהם מגדיר run מתנגשים כאשר מחלקה מנסה לרשת משניהם; שתי תכונות לעולם אינן מתנגשות.

כלל אצבע מעשי:

  • השתמשו בהורשה כאשר תת-המחלקה באמת היא סוג של הבסיס וכל מתודה בבסיס הגיונית גם עליה – Square הוא Shape; MemoryError הוא Exception.

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