מחלקה שבנויה על מחלקה אחרת
ירושה מאפשרת להגדיר מחלקה חדשה כהרחבה של מחלקה קיימת. המחלקה הקיימת היא האב (נקראת גם מחלקת בסיס או superclass), והחדשה היא הבת (subclass, תת-מחלקה). הבת מקבלת כל מתודה וכל תכונת מחלקה של האב בלי לחזור עליהן, והיא יכולה להוסיף משלה או להחליף את אלה שצריכות להתנהג אחרת.
מצהירים על האב בסוגריים אחרי שם המחלקה:
פלט:
Rex is an animal
Woof
ל-Dog אין __init__ ואין describe משלה. כשקוראים ל-Dog("Rex"), Python מחפשת __init__ ב-Dog, לא מוצאת, ומשתמשת בזו של Animal. אותו חיפוש קורה עבור rex.describe(). עבור rex.speak() היא מוצאת את speak ב-Dog קודם ועוצרת שם.
סדר החיפוש הזה הוא כל המנגנון: Python מחפשת במחלקה של האובייקט עצמו, אחר כך באב, אחר כך באב של האב, ומשתמשת בהתאמה הראשונה. אם מחלקות חדשות לכם, העמוד על מחלקות מכסה קודם את __init__, self ומתודות.
דריסת מתודה
מתודה בבת עם אותו שם כמו מתודה באב מחליפה אותה עבור מופעים של הבת. לזה קוראים דריסה (overriding). כל תת-מחלקה יכולה לענות על אותה קריאה בדרך שלה:
פלט:
Rectangle with area 12
Square with area 25
Shape with area 0
summary מוגדרת פעם אחת, ב-Shape, אבל היא קוראת ל-self.area(). מאחר ש-self הוא האובייקט בפועל, Rectangle מריצה את Rectangle.area, ו-Square מוצאת את area באב שלה, Rectangle. הקוד של האב קורא למתודה של הבת בלי לדעת שהבת בכלל קיימת. זה מה שהופך ירושה לשימושית לשיתוף תהליך עבודה משותף, כשכל תת-מחלקה משלימה את הפרטים.
קריאה לאב עם super()
כשבת מגדירה __init__ משלה, ה-__init__ של האב כבר לא רצה מעצמה. אם הבת עדיין צריכה את ההגדרה של האב, קראו לה עם super().__init__():
פלט:
Ada 1050.0
super() מחזירה אובייקט שמחפש מתודות החל מהאב של המחלקה הנוכחית. לכן super().__init__(owner, balance) מריצה את Account.__init__ על אותו אובייקט, ו-self.owner ו-self.balance קיימים אחר כך.
השמיטו את השורה הזו, והאובייקט בנוי רק למחצה:
פלט:
0.05
AttributeError: 'SavingsAccount' object has no attribute 'owner'
אפשר לכתוב במקום זאת Account.__init__(self, owner), וקוד ישן עושה את זה הרבה. עדיף super(), כי היא לא חוזרת על שם האב ומשתפת פעולה נכון עם ירושה מרובה, שנכסה בהמשך.
הרחבת מתודה במקום החלפתה
super() עובדת בכל מתודה, לא רק ב-__init__. קריאה לגרסה של האב בתוך מתודה דורסת מאפשרת להוסיף התנהגות סביבה במקום לכתוב אותה מחדש:
פלט:
started
[12:00] started
INFO [12:00] started
כל רמה מוסיפה חלק אחד ומאצילה את השאר כלפי מעלה. אם ה-format של האב ישתנה בהמשך, שתי הבנות יקבלו את השינוי.
isinstance ו-issubclass
מופע של תת-מחלקה הוא גם מופע של כל מחלקה שמעליה. isinstance(obj, cls) בודקת את היחס הזה, ו-issubclass(child, parent) בודקת אותו בין שתי מחלקות:
העדיפו את isinstance על פני השוואה ישירה של type(obj). בדיקה כמו type(obj) is Animal דוחה כל תת-מחלקה, וזה נשבר ברגע שמישהו מוסיף את Dog.
כל מחלקה שאתם כותבים יורשת מ-object, גם כשלא אומרים את זה. class Animal: זהה ל-class Animal(object):. משם מגיעות מתודות ברירת מחדל כמו __str__, __repr__ ו-__eq__, ולכן דריסה של __str__ משנה את מה ש-print() מציגה:
פלט:
(2, 3)
True
לחייב תת-מחלקות לממש מתודה
לפעמים אסור להשתמש באב ישירות, אלא רק כתבנית. המודול abc מסמן מחלקה כזו כמופשטת (abstract). כל מתודה שמעוטרת ב-@abstractmethod חייבת להידרס לפני שאפשר ליצור מופע של המחלקה:
שלוש השורות הראשונות של הפלט הן הייצוא:
exporting 2 rows
1,2
3,4
השורה האחרונה היא TypeError שמציינת את Exporter ואת המתודה החסרה שלה, export (הניסוח המדויק משתנה בין גרסאות Python). הטעות מתגלה כשהאובייקט נוצר, ולא מאוחר יותר, כשסוף סוף קוראים ל-export.
ירושה מרובה ו-MRO
מחלקה יכולה לציין יותר מאב אחד. אז היא יורשת מכולם:
פלט:
paddling flapping
swims
['Duck', 'Swimmer', 'Flyer', 'object']
שני ההורים מגדירים את move, ו-Duck מקבלת את זו של Swimmer. Python מחליטה לפי סדר רזולוציית המתודות (MRO, method resolution order): רשימה יחידה של מחלקות, שנשמרת ב-Duck.__mro__, שכל חיפוש תכונה עובר עליה מההתחלה לסוף. היא מתחילה במחלקה עצמה, שומרת על ההורים בסדר שבו ציינתם אותם, שמה כל מחלקה לפני ההורים שלה, ומסתיימת ב-object. החליפו את הסדר ל-class Duck(Flyer, Swimmer), ו-move תחזיר "flies".
super() עוקבת אחרי אותה רשימה. בתוך מתודה של Swimmer, super() פירושה "המחלקה הבאה אחרי Swimmer ב-MRO של המחלקה של האובייקט", שעבור Duck היא Flyer, ולא object. כך מחלקות משתפות פעולה יכולות כל אחת לקרוא ל-super(), וכל מחלקה בשרשרת רצה בדיוק פעם אחת, גם ביהלום שבו לשני הורים יש סבא משותף.
בפועל, ירושה מרובה משמשת בעיקר ל-mixins: מחלקות קטנות שמוסיפות יכולת אחת, כמו JsonMixin שמוסיפה מתודה to_json, בשילוב עם אב ראשי אחד. היררכיות עמוקות עם כמה הורים מלאים קשות למעקב ונחוצות לעיתים רחוקות.
ירושה או הרכבה?
ירושה ממדלת יחס של "הוא סוג של" (is a). SavingsAccount הוא סוג של Account: כל קוד שעובד עם Account אמור לעבוד איתו בלי שינוי. כשהמשפט הזה נשמע לא נכון, כנראה שאתם צריכים הרכבה (composition), שממדלת "יש לו" (has a): האובייקט שומר אובייקט אחר כתכונה וקורא לו.
פלט:
Roadster: 300 hp engine running
גם class Car(Engine) הייתה רצה, אבל היא הייתה טוענת שמכונית היא סוג של מנוע, נותנת ל-Car כל מתודה של מנוע, והופכת את החלפת המנוע לבלתי אפשרית בלי מחלקה אחרת. עם הרכבה החלקים נשארים נפרדים, והחלפת המנוע היא רק עניין של העברת אובייקט אחר.
כמה סימנים לכך שירושה היא הכלי הלא נכון:
- הבת דורסת את רוב המתודות של האב, או משביתה חלק מהן.
- אתם יורשים רק כדי לעשות שימוש חוזר במתודת עזר אחת או שתיים.
- ההיררכיה גדלה מעבר לשתיים או שלוש רמות, וצריך לקרוא כמה קבצים כדי להבין מה מתודה עושה.
טעויות נפוצות
- שוכחים את
super().__init__(). ה-__init__של הבת מחליפה את זו של האב, ולכן התכונות של האב אף פעם לא מוגדרות ומקבליםAttributeErrorמאוחר יותר. - מעבירים את
selfבקריאות ל-super(). כתבוsuper().__init__(name), ולאsuper().__init__(self, name).super()כבר קושרת את המתודה לאובייקט הנוכחי. - חתימות לא תואמות בדריסה. אם קוד משותף קורא ל-
speak(self)של האב,speak(self, volume)של הבת עם ארגומנט נוסף חובה שוברת את הקוד הזה. שמרו על דריסות תואמות, או תנו לפרמטרים החדשים ברירות מחדל. - בודקים
type(obj) == SomeClass. זה מתעלם מתת-מחלקות. השתמשו ב-isinstance. - משתמשים בירושה כדי לשתף קוד בין מחלקות שלא קשורות זו לזו. העבירו במקום זאת את הקוד המשותף לפונקציה או לאובייקט עזר.
שאלות נפוצות
איך יורשים ממחלקה ב-Python?
שימו את מחלקת האב בסוגריים אחרי שם המחלקה החדשה: class Dog(Animal):. Dog מקבלת כל מתודה וכל תכונת מחלקה של Animal, ויכולה להוסיף חדשות או לדרוס קיימות.
מה super().__init__() עושה?
היא קוראת ל-__init__ של מחלקת האב כדי שהאב יוכל להגדיר את התכונות שלו. כשתת-מחלקה מגדירה __init__ משלה, Python לא קוראת לזו של האב אוטומטית, ולכן קוראים ל-super().__init__(...) עם הארגומנטים שהאב צריך, ואז מגדירים את התכונות הנוספות של תת-המחלקה.
מה ההבדל בין isinstance ל-type ב-Python?
isinstance(obj, Animal) מחזירה True עבור Animal ועבור מופעים של כל תת-מחלקה של Animal. type(obj) is Animal מחזיר True רק כשהאובייקט נוצר מ-Animal עצמה. העדיפו את isinstance, כי היא ממשיכה לעבוד כשמישהו מוסיף תת-מחלקה.
האם Python תומכת בירושה מרובה?
כן: class C(A, B): יורשת משתיהן. כששני ההורים מגדירים את אותה מתודה, Python בוחרת אחת לפי סדר רזולוציית המתודות (MRO), שאפשר לבדוק עם C.__mro__. הוא מציג את C ראשונה, אחריה את ההורים בסדר שבו הם מופיעים בהגדרה, ואת object אחרונה.
מתי להשתמש בהרכבה (composition) במקום בירושה?
השתמשו בירושה ליחס של "הוא סוג של" (is a), שבו אפשר להשתמש בתת-המחלקה בכל מקום שבו מצפים לאב (SavingsAccount הוא סוג של Account). השתמשו בהרכבה, כלומר שמירת אובייקט כתכונה, ליחס של "יש לו" (has a) (ל-Car יש Engine). כשיש ספק, הרכבה קלה יותר לשינוי בהמשך.