ירושה ו-super: Classes דומות בלי קוד כפול

מטרות השיעור
  • להסביר מתי ירושה חוסכת קוד כפול, ולבדוק אותה במבחן “הוא סוג של”
  • להגדיר Parent ו-Child, ולהבין איך Python מחפשת מתודה — קודם ב-Child ואז ב-Parent
  • לדרוס מתודה (Override) ב-Child כדי לשנות התנהגות
  • להשתמש ב-super() — גם ב-__init__ וגם כדי להרחיב מתודה של ה-Parent

מה בשיעור

השיעור הזה בנוי כסיפור אחד, באותו סדר כמו במצגת:

  1. פרק ראשון — למה ירושה: אותו קוד פעמיים, ואיך יורשים במקום להעתיק
  2. פרק שני — דריסה ו-super(): ירושה, אבל לא בדיוק — משנים רק חלק מההתנהגות

בסוף השיעור — תרגול, ומיני פרויקט שמחבר את שני הפרקים.

פרק ראשון: למה ירושה — Parent, Child ויחס הוא-סוג-של

מה נלמד?

  • למה צריך ירושה: הבעיה של Classes כמעט זהות.
  • Parent Class (מחלקת אב / Base Class) ו-Child Class (מחלקת בן / Subclass).
  • התחביר של ירושה בפייתון.
  • מה עובר בירושה.
  • מבחן is-a: מתי ירושה מתאימה ומתי לא.

למה צריך את זה?

בנושא OOP – Classes ואובייקטים השבוע שאלנו: מה קורה כשמוסיפים “חשבון חיסכון” שדומה לחשבון רגיל, אבל עם ריבית? בגרסה הפרוצדורלית, כל פונקציה התמלאה ב-if account["type"] == .... בואו נראה את אותה בעיה ב-OOP, בלי ירושה:

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary * 1.25

    def describe(self):
        return self.name + " earns " + str(self.salary)


class Manager:
    def __init__(self, name, salary, team_size):
        self.name = name
        self.salary = salary
        self.team_size = team_size

    def monthly_cost(self):
        return self.salary * 1.25

    def describe(self):
        return self.name + " earns " + str(self.salary)

    def team_report(self):
        return self.name + " manages " + str(self.team_size) + " people"

Manager מעתיקה כמעט את כל Employee: name, salary, monthly_cost, describe. מה קורה כשעלות המעסיק משתנה מ-1.25 ל-1.3? צריך לתקן בשני מקומות, ואם יש גם Intern, Contractor ו-Director, בחמישה. זו הפרה של DRY ברמת ה-Classes.

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

הסבר מהבסיס

Inheritance (ירושה)

Inheritance = מנגנון שבו Class חדשה מקבלת אוטומטית את כל ה-Attributes והמתודות של Class קיימת, ויכולה להוסיף או לשנות חלקים.

  • Parent Class (מחלקת אב, Base, Superclass) — ה-Class הכללית. Employee.
  • Child Class (מחלקת בן, Derived, Subclass) — ה-Class הספציפית, שיורשת. Manager.
אנלוגיה: תבנית ייצור מכוניות — גרסה ספורטיבית

יש לנו תבנית ייצור של “מכונית” (Parent): 4 גלגלים, מנוע, היגוי, בלימה. עכשיו רוצים “מכונית ספורט”. לא מתכננים מכונית מאפס. לוקחים את התבנית הקיימת, ומוסיפים ספוילר ומשנים את המנוע. כל מכונית ספורט היא מכונית (נוסעת, בולמת), אבל לא כל מכונית היא מכונית ספורט.

יחס “הוא סוג של” (is-a)

ירושה מתאימה רק כשאפשר לומר בכנות: “Child הוא סוג של Parent”.

משפטירושה?
מנהל הוא סוג של עובד✓ Manager(Employee)
כלב הוא סוג של חיה✓ Dog(Animal)
חשבון חיסכון הוא סוג של חשבון בנק✓ SavingsAccount(BankAccount)
מכונית היא סוג של מנוע?✗ למכונית יש מנוע (has-a), וזו Composition, נושא תכנון OOP
הזמנה היא סוג של לקוח?✗ להזמנה יש לקוח

מה עובר בירושה?

ה-Child מקבלת הכול מה-Parent:

  • את __init__ (אם ה-Child לא מגדירה __init__ משלה).
  • את כל המתודות.
  • את ה-Class Attributes.

וה-Child יכולה:

  • להוסיף מתודות ו-Attributes חדשים.
  • לשנות (Override) מתודות קיימות. נלמד בפרק הבא, יחד עם super().

חיפוש בשכבות — שוב

כשכותבים manager.describe(), Python מחפשת: (1) על האובייקט, (2) ב-Class שלו (Manager), (3) ב-Parent (Employee)… ועוצרת במקום הראשון שבו מצאה. זה אותו רעיון של חיפוש שכבות שראינו ב-LEGB ובסדר החיפוש של Attributes.

תחביר

class Parent:
    def __init__(self, x):
        self.x = x

    def shared_method(self):
        return ...


class Child(Parent):             # Child inherits from Parent
    def extra_method(self):      # new method, only in Child
        return ...


c = Child(5)                     # uses Parent.__init__ (Child has none of its own)
c.shared_method()                # inherited
c.extra_method()                 # Child's own
  • שם ה-Parent בא בסוגריים אחרי שם ה-Child.
  • Child בלי שום תוספת: class Child(Parent): pass היא Class תקינה שזהה ל-Parent.

דוגמת קוד ראשונה

week-3/employees.py:

class Employee:
    EMPLOYER_COST_RATE = 1.25

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary * Employee.EMPLOYER_COST_RATE

    def describe(self):
        return self.name + " earns " + str(self.salary)


class Manager(Employee):
    def team_report(self, team_size):
        return self.name + " manages " + str(team_size) + " people"


dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)

print(dana.describe())
print(avi.describe())
print(avi.monthly_cost())
print(avi.team_report(5))

פלט:

Dana earns 12000
Avi earns 20000
25000.0
Avi manages 5 people

פירוק הקוד שורה אחר שורה

קודהסבר
class Manager(Employee):Manager יורשת מ-Employee.
Manager("Avi", 20000)ל-Manager אין __init__ משלה, ולכן Python מוצאת את זה של Employee ומשתמשת בו. אבי מקבל name ו-salary.
avi.describe()אין describe ב-Manager. Python עולה ל-Employee ומוצאת אותה. self הוא עדיין אבי.
avi.monthly_cost()אותו דבר, וגם EMPLOYER_COST_RATE נמצא דרך ה-Parent.
avi.team_report(5)מתודה שקיימת רק ב-Manager.
dana.team_report(...)היה זורק AttributeError: עובד רגיל לא יורש מהמנהל. הירושה היא מלמעלה למטה בלבד.

והתיקון של 1.25 → 1.3? משנים שורה אחת, ב-Employee, וכל המנהלים (ובעתיד, כל סוגי העובדים) מתעדכנים.

דוגמה נוספת

היררכיה עם כמה ילדים: Animal / Dog / Cat. week-3/animals.py:

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

    def birthday(self):
        self.age = self.age + 1

    def describe(self):
        return self.name + " (" + str(self.age) + " years old)"


class Dog(Animal):
    def fetch(self):
        return self.name + " brings back the ball"


class Cat(Animal):
    def climb(self):
        return self.name + " climbs the tree"


rex = Dog("Rex", 3)
mitzi = Cat("Mitzi", 5)

rex.birthday()
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())

for animal in [rex, mitzi]:
    print(type(animal).__name__, "->", animal.describe())

פלט:

Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
Dog -> Rex (4 years old)
Cat -> Mitzi (5 years old)

פירוק:

  • שני ילדים לאותו Parent: Dog ו-Cat חולקים את כל מה שמשותף (name, age, birthday, describe), וכל אחד מוסיף את מה שייחודי לו.
  • rex.birthday() — מתודה שנכתבה פעם אחת ב-Animal, ומשנה את ה-State של rex.
  • בלולאה, animal.describe() עובד על כל סוג של Animal, בלי לבדוק איזה סוג הוא. זה רמז ל-Polymorphism, שבו נעמיק בשיעור 3.
  • type(animal).__name__ — שם ה-Class כטקסט.
         Animal
        /      \
      Dog      Cat

סיכום

  • Inheritance = Child Class מקבלת את כל ה-Attributes והמתודות של Parent Class, ויכולה להוסיף ולשנות.
  • תחביר: class Child(Parent):.
  • פותרת כפילות בין Classes דומות (DRY), ומשקפת את היחס “הוא סוג של” (is-a).
  • Python מחפשת מתודה ב-Class של האובייקט, ואם לא מצאה, עולה ל-Parent.
  • הירושה חד-כיוונית: Parent לא מכירה את מה שנוסף ב-Child.
  • לא יורשים כשהיחס הוא “יש לו” (has-a). אז משתמשים ב-Composition (נושא תכנון OOP).

דוקומנטציה רשמית

פרק שני: דריסה ו-super() — משנים רק את מה ששונה

מה נלמד?

  • super(): איך Child משתמשת בקוד של ה-Parent.
  • __init__ ב-Child: להוסיף Attributes בלי להעתיק את של ה-Parent.
  • Method Overriding (דריסה): Child מחליפה מתודה של ה-Parent.
  • הרחבה: Child מוסיפה על המתודה של ה-Parent, במקום להחליף לגמרי.

למה צריך את זה?

בסוף השיעור הקודם נתקענו: ל-Truck צריך Attribute נוסף (cargo_kg), כלומר __init__ משלה. אבל אם נכתוב __init__ חדשה ב-Truck, היא מחליפה את זו של Vehicle, ואז brand ו-wheels לא יאותחלו, אלא אם נעתיק את הקוד. וזה בדיוק מה שירושה באה למנוע.

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

הסבר מהבסיס

Method Overriding — דריסה

אם ב-Child מגדירים מתודה באותו שם כמו ב-Parent, הגרסה של ה-Child מנצחת עבור אובייקטים מסוג Child. זה נקרא Override (דריסה). למה? זוכרים את סדר החיפוש: Python מחפשת קודם ב-Class של האובייקט, ורק אם לא מצאה, עולה ל-Parent. אם המתודה קיימת ב-Child, Python בכלל לא מגיעה לגרסה של ה-Parent.

super() — “הגרסה של ה-Parent”

super() מחזירה אובייקט מיוחד שדרכו קוראים למתודה של ה-Parent, על אותו self. שתי השימושים העיקריים:

  1. ב-__init__: super().__init__(...) — “תאתחל קודם את כל מה שה-Parent מאתחל”, ואז מוסיפים את מה שייחודי ל-Child.
  2. בהרחבת מתודה: super().describe() — “תביא לי את מה שה-Parent מחזיר”, ואז מוסיפים עליו.
אנלוגיה: מתכון משפחתי

סבתא (Parent) מכינה עוגת שוקולד לפי מתכון. אתם (Child) יורשים את המתכון. Override = אתם מחליטים להכין עוגה אחרת לגמרי באותו שם, “עוגת שוקולד”, אבל טבעונית, מאפס. הרחבה עם super() = “תעשו בדיוק כמו סבתא” (super().bake()), ואז להוסיף ציפוי שהמצאתם. לא מעתיקים את כל המתכון של סבתא. פשוט אומרים “כמו סבתא, ועוד…”.

שלוש אפשרויות למתודה ב-Child

אפשרותמה עושיםמתי
לרשתלא כותבים כלוםההתנהגות של ה-Parent מתאימה
לדרוס (Override)כותבים מתודה באותו שם, בלי super()ההתנהגות שונה לגמרי
להרחיב (Extend)כותבים מתודה באותו שם, ובתוכה super().method()“כמו ה-Parent, ועוד משהו”

תחביר

class Parent:
    def __init__(self, a):
        self.a = a

    def describe(self):
        return "a=" + str(self.a)

    def greet(self):
        return "Hello from Parent"


class Child(Parent):
    def __init__(self, a, b):
        super().__init__(a)          # let Parent set up self.a
        self.b = b                   # then add what is new in Child

    def describe(self):              # EXTEND: reuse Parent's result
        return super().describe() + ", b=" + str(self.b)

    def greet(self):                 # OVERRIDE: replace completely
        return "Hello from Child"

שימו לב: ב-super().__init__(a) לא מעבירים self. super() כבר “יודע” על איזה אובייקט מדובר.

דוגמת קוד ראשונה

פותרים את בעיית ה-Truck. week-3/vehicles_super.py:

class Vehicle:
    def __init__(self, brand, wheels):
        self.brand = brand
        self.wheels = wheels

    def describe(self):
        return self.brand + " with " + str(self.wheels) + " wheels"


class Truck(Vehicle):
    def __init__(self, brand, max_load_kg):
        super().__init__(brand, 6)
        self.max_load_kg = max_load_kg
        self.cargo_kg = 0

    def load(self, kg):
        if self.cargo_kg + kg > self.max_load_kg:
            raise ValueError("over the limit: max " + str(self.max_load_kg) + " kg")
        self.cargo_kg = self.cargo_kg + kg

    def describe(self):
        return super().describe() + ", carrying " + str(self.cargo_kg) + "/" + str(self.max_load_kg) + " kg"


volvo = Truck("Volvo", 1000)
volvo.load(600)
print(volvo.describe())

try:
    volvo.load(500)
except ValueError as error:
    print("Rejected:", error)

print(volvo.brand, volvo.wheels)

פלט:

Volvo with 6 wheels, carrying 600/1000 kg
Rejected: over the limit: max 1000 kg
Volvo 6

פירוק הקוד שורה אחר שורה

קודהסבר
def __init__(self, brand, max_load_kg):ל-Truck יש __init__ משלה, עם פרמטרים שמתאימים למשאית.
super().__init__(brand, 6)“תריץ את __init__ של Vehicle על האובייקט הזה”. זה יוצר self.brand ו-self.wheels. משאית תמיד עם 6 גלגלים, ולכן ה-Child מחליטה את הערך.
self.max_load_kg = ..., self.cargo_kg = 0רק מה שחדש ב-Truck. בלי להעתיק אף שורה מ-Vehicle.
def describe(self): ב-Truckהרחבה: אותו שם כמו ב-Vehicle.
super().describe()מקבל את "Volvo with 6 wheels" מה-Parent, ומוסיף לו את פרטי המטען.
volvo.brand, volvo.wheelsקיימים, כי super().__init__ יצר אותם.

אם Vehicle תשתנה מחר, למשל תוסיף self.year, משאיות יקבלו את זה אוטומטית דרך super().__init__. אילו העתקנו את הקוד, היינו צריכים לזכור לעדכן גם את Truck.

דוגמה נוספת

Override מלא, הרחבה, ו-super() בתוך כללים עסקיים. week-3/staff.py:

class Employee:
    EMPLOYER_COST_RATE = 1.25

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary * Employee.EMPLOYER_COST_RATE

    def title(self):
        return "Employee"

    def describe(self):
        return self.title() + " " + self.name + " - cost " + str(self.monthly_cost())


class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def monthly_cost(self):
        return super().monthly_cost() + self.bonus

    def title(self):
        return "Manager"


class Intern(Employee):
    def monthly_cost(self):
        return self.salary

    def title(self):
        return "Intern"


staff = [Employee("Dana", 12000), Manager("Avi", 20000, 3000), Intern("Noa", 5000)]

for person in staff:
    print(person.describe())

print("Total monthly cost:", sum(person.monthly_cost() for person in staff))

פלט:

Employee Dana - cost 15000.0
Manager Avi - cost 28000.0
Intern Noa - cost 5000
Total monthly cost: 48000.0

פירוק:

  • Manager.monthly_cost — הרחבה: “כמו עובד רגיל (super().monthly_cost() = 25,000), ועוד בונוס”. אם שיעור עלות המעסיק ישתנה, המנהלים יקבלו את השינוי אוטומטית.
  • Intern.monthly_cost — Override מלא: מתמחה לא נושא עלויות מעסיק, ולכן חישוב אחר לגמרי, בלי super().
  • title() — כל Class דורסת אותה.
  • הנקודה המעניינת: describe() כתובה רק פעם אחת, ב-Employee. היא קוראת ל-self.title() ול-self.monthly_cost(). כשהאובייקט הוא Manager, self.monthly_cost() מגיעה לגרסה של Manager! המתודה של ה-Parent משתמשת אוטומטית בגרסאות הדרוסות של ה-Child. זה הבסיס של Polymorphism (השיעור הבא).
  • Intern לא הגדירה __init__, ולכן משתמשת בזו של Employee (ירושה רגילה).

סיכום

  • Override = מתודה ב-Child באותו שם כמו ב-Parent. הגרסה של ה-Child מנצחת.
  • super() = גישה לגרסה של ה-Parent, על אותו self.
  • Child עם __init__ משלה חייבת לקרוא ל-super().__init__(...) (בלי self), ואז להוסיף רק את מה שחדש.
  • הרחבה: super().method() + תוספת. דריסה מלאה: בלי super().
  • self.method() בתוך מתודה דרוסה = רקורסיה. לגרסת ה-Parent: super().method().
  • מתודה של ה-Parent שקוראת ל-self.x() תגיע אוטומטית לגרסה הדרוסה של ה-Child.
  • מתודה דרוסה שומרת על אותה חתימה (אותם פרמטרים).

דוקומנטציה רשמית

תרגול

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

מה מתרגלים בדף הזה: ירושה — Parent ו-Child, מבחן “הוא סוג של”, הרחבה של __init__ עם super(), ודריסה של מתודה (Override).

בסוף הדף תדעו:

  • לכתוב Child שיורש מ-Parent ומוסיף רק את מה שחדש
  • להחליט מתי ירושה מתאימה, בעזרת המבחן “הוא סוג של”
  • לקרוא ל-super().__init__ כדי שה-Parent יאתחל את החלק שלו
  • לדרוס מתודה, ולהשתמש בגרסה של ה-Parent בתוכה

תרגיל 1: עובד ומנהל

מה בונים: עובד ומנהל — כשמנהל הוא סוג של עובד, ולכן לא כותבים שוב את מה שמשותף.

מה לכתוב:

  1. Class בשם Employee עם שם ומשכורת.
  2. ב-Employee: describe שמחזירה שם ומשכורת, כמו בפלט.
  3. Class בשם Manager שיורשת מ-Employee — בלי __init__ משלה.
  4. ב-Manager בלבד: manage שמקבלת גודל צוות ומחזירה משפט כמו בפלט.

קוד לבדיקה:

dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)
print(dana.describe())
print(avi.describe())
print(avi.manage(5))

הפלט הרצוי:

Dana: 12000
Avi: 20000
Avi manages 5 people
הצג פתרון
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

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


class Manager(Employee):
    def manage(self, team_size):
        return self.name + " manages " + str(team_size) + " people"


dana = Employee("Dana", 12000)
avi = Manager("Avi", 20000)
print(dana.describe())
print(avi.describe())
print(avi.manage(5))
Dana: 12000
Avi: 20000
Avi manages 5 people

Manager יורש את __init__ ואת describe — בלי לכתוב אותם שוב — ומוסיף רק את מה שחדש.

תרגיל 2: is-a — הוא סוג של

מה עושים: מחליטים — בלי קוד — מתי ירושה מתאימה.

המשימה: לכל זוג, ענו: האם הראשון צריך לרשת מהשני? הסבירו במשפט “X הוא סוג של Y” — ואם המשפט לא נכון, מה כן הקשר ביניהם.

  1. Dog ו-Animal
  2. Car ו-Engine
  3. SavingsAccount ו-BankAccount
  4. Square ו-Color

מה מגישים: כן או לא לכל זוג, ומשפט הסבר אחד.

הצג פתרון
  1. כן — כלב הוא סוג של חיה.
  2. לא — מכונית לא סוג של מנוע. למכונית יש מנוע (has-a). זה מקרה להרכבה, לא לירושה.
  3. כן — חשבון חיסכון הוא סוג של חשבון בנק.
  4. לא — ריבוע לא סוג של צבע. אולי לריבוע יש צבע.

ירושה רק כשהמשפט “X הוא סוג של Y” נכון.

תרגיל 3: עץ חיות

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

מה לכתוב:

  1. Class בשם Animal עם שם וגיל, ו-describe שמחזירה שם וגיל כמו בפלט.
  2. Class בשם Dog שיורשת מ-Animal, עם fetch שמחזירה את המשפט שבפלט.
  3. Class בשם Cat שיורשת מ-Animal, עם climb שמחזירה את המשפט שבפלט.

קוד לבדיקה:

rex = Dog("Rex", 4)
mitzi = Cat("Mitzi", 5)
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())

הפלט הרצוי:

Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
הצג פתרון
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def describe(self):
        return self.name + " (" + str(self.age) + " years old)"


class Dog(Animal):
    def fetch(self):
        return self.name + " brings back the ball"


class Cat(Animal):
    def climb(self):
        return self.name + " climbs the tree"


rex = Dog("Rex", 4)
mitzi = Cat("Mitzi", 5)
print(rex.describe(), "|", rex.fetch())
print(mitzi.describe(), "|", mitzi.climb())
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree

תרגיל 4: super().init

מה בונים: משאית — רכב שתמיד יש לו 6 גלגלים, ויש לו גם משקל מטען מקסימלי.

מה לכתוב:

  1. Class בשם Vehicle עם מותג ומספר גלגלים.
  2. Class בשם Truck שיורשת מ-Vehicle. ה-__init__ שלה מקבל מותג ומשקל מקסימלי.
  3. בתוך __init__ של Truck: קוראים ל-super().__init__ עם המותג ו-6 גלגלים, ומאתחלים משקל נוכחי 0.
  4. load — מקבלת קילוגרמים ומוסיפה למשקל. אם עוברים את המקסימום: ValueError עם ההודעה שבפלט.

קוד לבדיקה:

truck = Truck("Volvo", 1000)
truck.load(600)
print(truck.brand, truck.wheels, truck.load_kg)
try:
    truck.load(500)
except ValueError as error:
    print("error:", error)

הפלט הרצוי:

Volvo 6 600
error: overloaded: max 1000 kg
הצג פתרון
class Vehicle:
    def __init__(self, brand, wheels):
        self.brand = brand
        self.wheels = wheels


class Truck(Vehicle):
    def __init__(self, brand, max_load_kg):
        super().__init__(brand, 6)
        self.max_load_kg = max_load_kg
        self.load_kg = 0

    def load(self, kg):
        if self.load_kg + kg > self.max_load_kg:
            raise ValueError("overloaded: max " + str(self.max_load_kg) + " kg")
        self.load_kg = self.load_kg + kg


truck = Truck("Volvo", 1000)
truck.load(600)
print(truck.brand, truck.wheels, truck.load_kg)
try:
    truck.load(500)
except ValueError as error:
    print("error:", error)
Volvo 6 600
error: overloaded: max 1000 kg

super().__init__(brand, 6) מאתחל את החלק של ה-Parent. אחר כך ה-Child מוסיף את ה-Attributes שלו.

תרגיל 5: Override עם super

מה בונים: מנהל שהעלות החודשית שלו היא המשכורת ועוד בונוס.

מה לכתוב:

  1. Class בשם Employee עם שם ומשכורת, ו-monthly_cost שמחזירה את המשכורת.
  2. Class בשם Manager שיורשת מ-Employee, ומקבלת גם בונוס. ב-__init__ שלה קוראים ל-super().__init__.
  3. ב-Manager דורסים את monthly_cost: הגרסה של ה-Parent, דרך super(), ועוד הבונוס.

קוד לבדיקה:

print(Employee("Dana", 12000).monthly_cost())
print(Manager("Avi", 20000, 3000).monthly_cost())

הפלט הרצוי:

12000
23000
הצג פתרון
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def monthly_cost(self):
        return self.salary


class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def monthly_cost(self):
        return super().monthly_cost() + self.bonus


print(Employee("Dana", 12000).monthly_cost())
print(Manager("Avi", 20000, 3000).monthly_cost())
12000
23000

super().monthly_cost() קורא לגרסה של ה-Parent. אם החישוב של המשכורת הבסיסית ישתנה, Manager יקבל את השינוי אוטומטית.

מיני פרויקט: עובדים בחברה

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

מה תלמדו מהפרויקט:

  • לבנות Parent עם כל מה שמשותף, ו-Children שמוסיפים רק את מה ששונה
  • לקרוא ל-super().__init__ מתוך ה-Child
  • לדרוס מתודה (Override), ולהרחיב מתודה עם super()
  • לראות איך מתודה שנכתבה פעם אחת ב-Parent עובדת נכון לכל סוג

מה צריך לבנות — שלב אחרי שלב:

שלב 1 — עובד (Employee). __init__ מקבל שם ומשכורת. שלוש מתודות:

  • title — מחזירה את המילה Employee;
  • monthly_cost — מחזירה את המשכורת;
  • describe — מחזירה שם, תפקיד ועלות חודשית, כמו בפלט. היא משתמשת ב-title וב-monthly_cost.

שלב 2 — מנהל (Manager). יורש מ-Employee ומקבל גם בונוס. ב-__init__ קוראים ל-super().__init__ ושומרים את הבונוס. דורסים את title — המילה Manager. מרחיבים את monthly_cost: התוצאה של super() ועוד הבונוס.

שלב 3 — מתמחה (Intern). יורש מ-Employee ומקבל גם מספר חודשי התמחות. ב-__init__ בודקים שהמספר בין 1 ל-12 — אחרת ValueError — ואז קוראים ל-super().__init__. דורסים את title — המילה Intern. את monthly_cost ואת describe לא כותבים: הן עוברות בירושה.

הודעת השגיאה — בדיוק כך:

months must be 1-12

קוד לבדיקה:

staff = [
    Employee("Dana", 12000),
    Manager("Avi", 20000, 3000),
    Intern("Noa", 6000, 6),
]

for person in staff:
    print(person.describe())

try:
    Intern("Tom", 5000, 0)
except ValueError as error:
    print("error:", error)

total = 0
for person in staff:
    total = total + person.monthly_cost()
print("Total monthly cost:", total)

הפלט הרצוי:

Dana (Employee): 12000
Avi (Manager): 23000
Noa (Intern): 6000
error: months must be 1-12
Total monthly cost: 41000

בסוף הפרויקט תדעו:

  • לתכנן היררכיה: מה עולה ל-Parent ומה נשאר ב-Child
  • להבחין בין שלוש אפשרויות למתודה ב-Child: לרשת, לדרוס, או להרחיב עם super()
  • להבין למה describe מציגה לכל עובד את התפקיד והעלות שלו, למרות שנכתבה פעם אחת

בונוס: הוסיפו ל-Manager מתודה manage שמקבלת גודל צוות ומחזירה משפט כמו Avi manages 5 people, והדפיסו אותה רק עבור המנהלים ברשימה.

הצג פתרון
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def title(self):
        return "Employee"

    def monthly_cost(self):
        return self.salary

    def describe(self):
        return self.name + " (" + self.title() + "): " + str(self.monthly_cost())


class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def title(self):
        return "Manager"

    def monthly_cost(self):
        return super().monthly_cost() + self.bonus


class Intern(Employee):
    def __init__(self, name, salary, months):
        if months < 1 or months > 12:
            raise ValueError("months must be 1-12")
        super().__init__(name, salary)
        self.months = months

    def title(self):
        return "Intern"


staff = [
    Employee("Dana", 12000),
    Manager("Avi", 20000, 3000),
    Intern("Noa", 6000, 6),
]

for person in staff:
    print(person.describe())

try:
    Intern("Tom", 5000, 0)
except ValueError as error:
    print("error:", error)

total = 0
for person in staff:
    total = total + person.monthly_cost()
print("Total monthly cost:", total)
Dana (Employee): 12000
Avi (Manager): 23000
Noa (Intern): 6000
error: months must be 1-12
Total monthly cost: 41000
  • describe נכתבה פעם אחת ב-Employee, אבל כל עובד מקבל את ה-title() ואת ה-monthly_cost() שלו — כי Python מחפשת את המתודה קודם ב-Child.
  • Manager.monthly_cost מרחיבה: super().monthly_cost() מחזירה את המשכורת, ומוסיפים את הבונוס. title דורסת לגמרי.
  • Intern לא כותב את monthly_cost — הוא יורש אותה כמו שהיא.

בונוס:

    def manage(self, team_size):
        return self.name + " manages " + str(team_size) + " people"

(מתודה בתוך Manager.)

for person in staff:
    if person.title() == "Manager":
        print(person.manage(5))
Avi manages 5 people