2.22. הורשה¶
הורשה מאפשרת למחלקה להרחיב מחלקה אחרת – להתחיל עם כל המתודות והתכונות שלה, ואז להוסיף או לדרוס את מה ששונה. המקורית נקראת הבסיס (או ההורה); ההרחבה נקראת התת-מחלקה.
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.השתמשו בקומפוזיציה עבור כל השאר – כאשר אתם רק רוצים את ההתנהגות של מחלקה אחרת, לא את הזהות שלה. רוב הקוד האמיתי משתמש בקומפוזיציה הרבה יותר מאשר בהורשה.