# ירושה ו-super: Classes דומות בלי קוד כפול<no value>


## מה בשיעור

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

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

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

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

### מה נלמד?

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

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

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

```python
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`.

{{< box "analogy" "אנלוגיה: תבנית ייצור מכוניות — גרסה ספורטיבית" >}}
יש לנו תבנית ייצור של "מכונית" (Parent): 4 גלגלים, מנוע, היגוי, בלימה. עכשיו רוצים "מכונית ספורט". **לא מתכננים מכונית מאפס.** לוקחים את התבנית הקיימת, ו**מוסיפים** ספוילר ו**משנים** את המנוע. כל מכונית ספורט **היא מכונית** (נוסעת, בולמת), אבל לא כל מכונית היא מכונית ספורט.
{{< /box >}}

#### יחס "הוא סוג של" (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.

### תחביר

```python
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`:

```python
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))
```

**פלט:**

```text
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`:

```python
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())
```

**פלט:**

```text
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 כטקסט.

```text
         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).

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

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

- [Python Tutorial — Inheritance](https://docs.python.org/3/tutorial/classes.html#inheritance) — Parent ו-Child
- [isinstance()](https://docs.python.org/3/builtins/functions.html#isinstance) — בדיקת יחס הוא-סוג-של

<!-- docs:end -->

## פרק שני: דריסה ו-`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 מחזיר", ואז מוסיפים עליו.

{{< box "analogy" "אנלוגיה: מתכון משפחתי" >}}
סבתא (Parent) מכינה עוגת שוקולד לפי מתכון. אתם (Child) **יורשים** את המתכון. **Override** = אתם מחליטים להכין עוגה אחרת לגמרי באותו שם, "עוגת שוקולד", אבל טבעונית, מאפס. **הרחבה עם `super()`** = "תעשו בדיוק כמו סבתא" (`super().bake()`), **ואז** להוסיף ציפוי שהמצאתם. לא מעתיקים את כל המתכון של סבתא. פשוט אומרים "כמו סבתא, ועוד...".
{{< /box >}}

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

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

### תחביר

```python
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`:

```python
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)
```

**פלט:**

```text
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`:

```python
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))
```

**פלט:**

```text
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.
- מתודה דרוסה שומרת על **אותה חתימה** (אותם פרמטרים).

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

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

- [super()](https://docs.python.org/3/builtins/functions.html#super) — קריאה ל-Method של ה-Parent
- [Python Tutorial — Inheritance](https://docs.python.org/3/tutorial/classes.html#inheritance) — דריסת Methods

<!-- docs:end -->

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

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

**מה מתרגלים בדף הזה:** ירושה — 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` שמקבלת גודל צוות ומחזירה משפט כמו בפלט.

**קוד לבדיקה:**

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

**הפלט הרצוי:**

```text
Dana: 12000
Avi: 20000
Avi manages 5 people
```

{{< solution >}}
```python
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))
```

```text
Dana: 12000
Avi: 20000
Avi manages 5 people
```

`Manager` **יורש** את `__init__` ואת `describe` — בלי לכתוב אותם שוב — ומוסיף רק את מה שחדש.
{{< /solution >}}

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

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

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

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

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

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

ירושה רק כשהמשפט "X הוא סוג של Y" נכון.
{{< /solution >}}

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

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

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

**קוד לבדיקה:**

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

**הפלט הרצוי:**

```text
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
```

{{< solution >}}
```python
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())
```

```text
Rex (4 years old) | Rex brings back the ball
Mitzi (5 years old) | Mitzi climbs the tree
```
{{< /solution >}}

### תרגיל 4: super().__init__

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

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

**קוד לבדיקה:**

```python
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)
```

**הפלט הרצוי:**

```text
Volvo 6 600
error: overloaded: max 1000 kg
```

{{< solution >}}
```python
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)
```

```text
Volvo 6 600
error: overloaded: max 1000 kg
```

`super().__init__(brand, 6)` מאתחל את החלק של ה-Parent. אחר כך ה-Child מוסיף את ה-Attributes שלו.
{{< /solution >}}

### תרגיל 5: Override עם super

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

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

**קוד לבדיקה:**

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

**הפלט הרצוי:**

```text
12000
23000
```

{{< solution >}}
```python
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())
```

```text
12000
23000
```

`super().monthly_cost()` קורא לגרסה של ה-Parent. אם החישוב של המשכורת הבסיסית ישתנה, `Manager` יקבל את השינוי אוטומטית.
{{< /solution >}}

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

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

**מה תלמדו מהפרויקט:**
- לבנות 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` **לא** כותבים: הן עוברות בירושה.

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

```text
months must be 1-12
```

**קוד לבדיקה:**

```python
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)
```

**הפלט הרצוי:**

```text
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, והדפיסו אותה רק עבור המנהלים ברשימה.

{{< solution >}}
```python
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)
```

```text
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` — הוא **יורש** אותה כמו שהיא.

**בונוס:**

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

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

```python
for person in staff:
    if person.title() == "Manager":
        print(person.manage(5))
```

```text
Avi manages 5 people
```
{{< /solution >}}

<!-- exercises:end -->
