# Polymorphism: ממשק משותף, התנהגות שונה<no value>

## מה נלמד?

- מהו **Polymorphism** (רב-צורתיות).
- **ממשק משותף** (Common Interface): אותה מתודה, התנהגות שונה לכל סוג.
- למה זה **מחליף** את ה-`if type == ...`.
- **Duck Typing**: הגרסה הגמישה של Python.

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

נניח שמערכת תשלומים צריכה לחייב לקוח, והלקוח יכול לשלם בכרטיס אשראי, ב-PayPal או בהעברה בנקאית. בלי Polymorphism:

```python
def pay(method, amount):
    if method["type"] == "credit":
        print("Charging card", method["card"], amount)
    elif method["type"] == "paypal":
        print("Redirecting", method["email"], "to PayPal for", amount)
    elif method["type"] == "bank":
        print("Waiting for transfer of", amount, "to account", method["account"])
```

ומה קורה כשמוסיפים Apple Pay? צריך להוסיף `elif` **כאן**, וגם בפונקציה של החזרים, וגם בדוח החודשי, וגם בבדיקת העמלות... כל פונקציה שמטפלת בתשלומים מכילה את אותה שרשרת `if`. **שוכחים אחד, ויש באג.**

ביום הראשון של השבוע זו הייתה "בעיה 4". Polymorphism הוא הפתרון שלה.

## הסבר מהבסיס

### מהו Polymorphism?

**Polymorphism** (מיוונית: "צורות רבות") = **היכולת לעבוד עם אובייקטים מסוגים שונים דרך אותו ממשק**, כשכל אובייקט מבצע את הפעולה **בדרך שלו**.

במילים פשוטות: **אותה קריאה** (`method.pay(100)`) ‏←‏ **התנהגות שונה**, לפי סוג האובייקט. הקוד שקורא **לא צריך לדעת** איזה סוג יש לו ביד.

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

### איך זה עובד: ממשק משותף

1. מגדירים ב-**Parent** את **הממשק**: אילו מתודות יש לכל הסוגים (`pay`, `refund`, `fee`).
2. כל **Child** **דורסת** (Override) את המתודות בדרך שלה.
3. הקוד שמשתמש עובד מול **הממשק**: `method.pay(amount)`, ולא מול הסוגים.

**כבר ראינו את זה!** ב-`describe()` של Employee, שקראה ל-`self.monthly_cost()` והגיעה לגרסה הנכונה של מנהל או מתמחה. וב-`describe()` של Shape שעבד לכל צורה.

### היתרון העיקרי: Open/Closed

עם Polymorphism, הוספת סוג חדש (Apple Pay) = **כתיבת Class חדשה אחת**. **אף שורה** בקוד הקיים שמשתמש בתשלומים לא משתנה. המערכת **פתוחה להרחבה וסגורה לשינוי**, עיקרון חשוב בתכנון תוכנה.

### Duck Typing

ב-Python, Polymorphism לא **מחייב** ירושה. Python לא בודקת "האם האובייקט יורש מ-PaymentMethod?". היא רק מנסה לקרוא למתודה. אם המתודה קיימת, זה עובד.

> "If it walks like a duck and quacks like a duck, it's a duck."
> (אם זה הולך כמו ברווז ומגעגע כמו ברווז, זה ברווז.)

**Duck Typing** = "לא משנה מה **אתה**, משנה מה אתה **יודע לעשות**". כבר השתמשנו בזה: `len()` עובדת על `str`, `list`, `dict` ו-`set`, כי לכולם יש "אורך". `for` עובד על כל מה שאפשר לעבור עליו.

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

## תחביר

```python
class Base:
    def action(self):                    # the common interface
        raise NotImplementedError("subclasses must implement action()")


class TypeA(Base):
    def action(self):
        return "A does it this way"


class TypeB(Base):
    def action(self):
        return "B does it another way"


for item in [TypeA(), TypeB()]:          # polymorphic code:
    print(item.action())                 # one call, many behaviors, no if/elif
```

**`raise NotImplementedError`** ב-Parent = "כל Child **חייבת** לממש את המתודה הזו". אם Child שכחה, מקבלים שגיאה ברורה ברגע הקריאה.

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

**מערכת התשלומים, עם Polymorphism.** `week-3/payments.py`:

```python
class PaymentMethod:
    def __init__(self, owner):
        self.owner = owner

    def pay(self, amount):
        raise NotImplementedError("subclasses must implement pay()")

    def fee(self, amount):
        return 0


class CreditCard(PaymentMethod):
    def __init__(self, owner, last_digits):
        super().__init__(owner)
        self.last_digits = last_digits

    def pay(self, amount):
        return "Charged " + str(amount) + " to card ****" + self.last_digits

    def fee(self, amount):
        return round(amount * 0.02, 2)


class PayPal(PaymentMethod):
    def __init__(self, owner, email):
        super().__init__(owner)
        self.email = email

    def pay(self, amount):
        return "Sent " + str(amount) + " via PayPal from " + self.email


class BankTransfer(PaymentMethod):
    def pay(self, amount):
        return "Waiting for a bank transfer of " + str(amount) + " from " + self.owner


def checkout(method, amount):
    total = amount + method.fee(amount)
    print(method.pay(total), "| fee:", method.fee(amount))


methods = [CreditCard("Dana", "4321"), PayPal("Avi", "avi@mail.com"), BankTransfer("Noa")]
for method in methods:
    checkout(method, 100)
```

**פלט:**

```text
Charged 102.0 to card ****4321 | fee: 2.0
Sent 100 via PayPal from avi@mail.com | fee: 0
Waiting for a bank transfer of 100 from Noa | fee: 0
```

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

| קוד | הסבר |
|---|---|
| `class PaymentMethod` | **הממשק**: כל אמצעי תשלום **חייב** `pay`, ויש לו `fee` (ברירת מחדל 0). |
| `raise NotImplementedError` | "מתודה מופשטת": מגדירה ממשק, בלי מימוש. |
| `CreditCard.pay`, `PayPal.pay`, `BankTransfer.pay` | כל אחד מממש את `pay` **בדרך שלו**. |
| `CreditCard.fee` | Override: עמלה של 2%. האחרים **יורשים** את ה-0 של ה-Parent. |
| `def checkout(method, amount):` | **הקוד הפולימורפי**: לא בודק את הסוג, לא מכיל `if`. רק קורא ל-`method.fee()` ול-`method.pay()`. |
| הלולאה | אותה פונקציה בדיוק, שלוש התנהגויות שונות. |

## דוגמה נוספת

**הוספת סוג חדש — בלי לגעת בקוד קיים.** ואז Duck Typing.

```python
class ApplePay(PaymentMethod):
    def __init__(self, owner, device):
        super().__init__(owner)
        self.device = device

    def pay(self, amount):
        return "Paid " + str(amount) + " with Apple Pay on " + self.device

    def fee(self, amount):
        return 1


checkout(ApplePay("Tom", "iPhone 15"), 100)
```

```text
Paid 101 with Apple Pay on iPhone 15 | fee: 1
```

**`checkout` לא השתנתה בכלל.** הוספנו Class, והיא עובדת. בגרסת ה-`if`/`elif` היינו צריכים לערוך כל פונקציה שמטפלת בתשלומים.

**Duck Typing — אובייקט שלא יורש מ-PaymentMethod:**

```python
class GiftCard:
    def __init__(self, code):
        self.code = code

    def pay(self, amount):
        return "Redeemed " + str(amount) + " from gift card " + self.code

    def fee(self, amount):
        return 0


checkout(GiftCard("GIFT-2026"), 100)
```

```text
Redeemed 100 from gift card GIFT-2026 | fee: 0
```

`GiftCard` **לא** יורשת מ-`PaymentMethod`, אבל יש לה `pay` ו-`fee`, ולכן `checkout` עובדת איתה. זה Duck Typing. (בפרויקט אמיתי עדיף בכל זאת שתירש, כדי שהממשק יהיה מתועד ומפורש.)

### עוד דוגמה מוכרת: הודעות

```python
class Notification:
    def __init__(self, recipient):
        self.recipient = recipient

    def send(self, text):
        raise NotImplementedError


class EmailNotification(Notification):
    def send(self, text):
        return "Email to " + self.recipient + ": " + text


class SmsNotification(Notification):
    def send(self, text):
        return "SMS to " + self.recipient + ": " + text[:20]


def notify_all(channels, text):
    for channel in channels:
        print(channel.send(text))


notify_all([EmailNotification("dana@mail.com"), SmsNotification("050-1234567")],
           "Your order #1042 has shipped and will arrive tomorrow")
```

```text
Email to dana@mail.com: Your order #1042 has shipped and will arrive tomorrow
SMS to 050-1234567: Your order #1042 has
```

לכל ערוץ כללים משלו (SMS קוצץ ל-20 תווים), ו-`notify_all` לא יודעת ולא אכפת לה.

## טעות נפוצה

**הטעות 1: לבדוק סוג במקום להשתמש ב-Polymorphism.**

```python
def checkout(method, amount):
    if isinstance(method, CreditCard):
        ...
    elif isinstance(method, PayPal):
        ...
```

זה מחזיר אותנו בדיוק לבעיה המקורית, רק עם Classes. אם מוצאים את עצמכם כותבים `if isinstance(...)` על Classes **שלכם** כדי להחליט **איך לבצע פעולה**, זה סימן שהפעולה צריכה להיות **מתודה** שכל Class מממשת.

**הטעות 2: לשכוח לממש מתודה מופשטת.**

```python
class Cash(PaymentMethod):
    pass


checkout(Cash("Rina"), 50)
```

```text
NotImplementedError: subclasses must implement pay()
```

וזה **טוב**: השגיאה ברורה ומצביעה בדיוק על מה שחסר. בלי ה-`raise` ב-Parent, הייתה מתקבלת התנהגות שקטה ושגויה, או `None` במקום מחרוזת.

**הטעות 3: ממשק לא אחיד בין הסוגים.**

```python
class CreditCard(PaymentMethod):
    def pay(self, amount, cvv):
        ...
```

עכשיו `checkout` (שקוראת `method.pay(total)`) נכשלת רק על כרטיסי אשראי. **כל המימושים צריכים לקבל את אותם פרמטרים.** מידע שספציפי לסוג (כמו CVV) שייך ל-`__init__` או ל-Attribute של אותו סוג.

**הטעות 4: לחשוב ש-Polymorphism מחייב ירושה.** ב-Python לא (Duck Typing). אבל ירושה מ-Base משותף **מתעדת** את הממשק, ומונעת טעויות. העדיפו אותה כשמתכננים.

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

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

**מה מתרגלים בדף הזה:** פולימורפיזם — אותה קריאה לכמה סוגים, כשכל סוג מתנהג אחרת; הוספת סוג חדש בלי לשנות קוד קיים; Duck Typing ו-`NotImplementedError`.

**בסוף הדף תדעו:**
- להחליף שרשרת `if` לפי סוג במתודה משותפת שכל סוג מממש
- להוסיף סוג חדש בלי לגעת בקוד שמשתמש בו
- להסביר מה זה Duck Typing
- לחייב Children לממש מתודה בעזרת `NotImplementedError`

### תרגיל 1: במקום שרשרת if

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

**הקוד שמחליפים:**

```text
def pay(method, amount):
    if method["type"] == "credit":
        return "Charging card " + str(amount)
    elif method["type"] == "paypal":
        return "Redirecting to PayPal " + str(amount)
```

**מה לכתוב:**
1. Class בסיס בשם `PaymentMethod`, עם `pay` שמקבלת סכום וזורקת `NotImplementedError`.
2. שני Children: `CreditCard` ו-`PayPal`. כל אחד דורס את `pay` ומחזיר את המשפט שלו מהפלט.
3. פונקציה בשם `checkout` שמקבלת אמצעי תשלום וסכום, וקוראת ל-`pay` — בלי `if` על הסוג.

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

```python
for method in [CreditCard(), PayPal()]:
    print(checkout(method, 100))
```

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

```text
Charging card 100
Redirecting to PayPal 100
```

{{< solution >}}
```python
class PaymentMethod:
    def pay(self, amount):
        raise NotImplementedError("subclasses must implement pay()")


class CreditCard(PaymentMethod):
    def pay(self, amount):
        return "Charging card " + str(amount)


class PayPal(PaymentMethod):
    def pay(self, amount):
        return "Redirecting to PayPal " + str(amount)


def checkout(method, amount):
    return method.pay(amount)


for method in [CreditCard(), PayPal()]:
    print(checkout(method, 100))
```

```text
Charging card 100
Redirecting to PayPal 100
```

`checkout` לא יודעת ולא צריכה לדעת איזה סוג תשלום זה — היא רק קוראת ל-`pay`. כל סוג מממש אותה אחרת. זה **Polymorphism**.
{{< /solution >}}

### תרגיל 2: סוג חדש — בלי לגעת ב-checkout

**מה בונים:** ממשיכים את הקופה — מוסיפים אמצעי תשלום חדש ועמלה.

**מה לכתוב:**
1. Child חדש בשם `ApplePay`, עם `pay` שמחזירה את המשפט שלו מהפלט.
2. ב-`PaymentMethod` מוסיפים `fee` שמקבלת סכום ומחזירה 0 — ברירת מחדל לכולם.
3. ב-`CreditCard` דורסים את `fee`: עמלה של 2% מהסכום.
4. מעדכנים את `checkout` כך שתחזיר גם את העמלה, כמו בפלט.

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

```python
for method in [CreditCard(), PayPal(), ApplePay()]:
    print(checkout(method, 100))
```

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

```text
Charging card 100 | fee: 2.0
Redirecting to PayPal 100 | fee: 0
Paid with Apple Pay 100 | fee: 0
```

{{< solution >}}
```python
class PaymentMethod:
    def pay(self, amount):
        raise NotImplementedError("subclasses must implement pay()")

    def fee(self, amount):
        return 0


class CreditCard(PaymentMethod):
    FEE_PERCENT = 2

    def pay(self, amount):
        return "Charging card " + str(amount)

    def fee(self, amount):
        return amount * CreditCard.FEE_PERCENT / 100


class PayPal(PaymentMethod):
    def pay(self, amount):
        return "Redirecting to PayPal " + str(amount)


class ApplePay(PaymentMethod):
    def pay(self, amount):
        return "Paid with Apple Pay " + str(amount)


def checkout(method, amount):
    return method.pay(amount) + " | fee: " + str(method.fee(amount))


for method in [CreditCard(), PayPal(), ApplePay()]:
    print(checkout(method, 100))
```

```text
Charging card 100 | fee: 2.0
Redirecting to PayPal 100 | fee: 0
Paid with Apple Pay 100 | fee: 0
```

`ApplePay` נוסף בלי לשנות אף שורה ב-`checkout`. `PayPal` ו-`ApplePay` יורשים את `fee` של ה-Parent, ו-`CreditCard` דורס אותה.
{{< /solution >}}

### תרגיל 3: דמויות במשחק

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

**מה לכתוב:**
1. Class בסיס בשם `Character`, עם שם וחיים, ו-`attack_power` שזורקת `NotImplementedError`.
2. `Warrior` — כוח ההתקפה שלו תמיד 15.
3. `Mage` — כוח ההתקפה שלו 10, ועוד 10 אם יש לו יותר מ-50 חיים.
4. פונקציה בשם `attack` שמקבלת תוקף ונתקף: מורידה לנתקף חיים לפי כוח התוקף, ומדפיסה משפט כמו בפלט.

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

```python
conan = Warrior("Conan", 100)
merlin = Mage("Merlin", 60)
attack(conan, merlin)
attack(merlin, conan)
print(merlin.name, merlin.health)
print(conan.name, conan.health)
```

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

```text
Conan hits Merlin for 15
Merlin hits Conan for 10
Merlin 45
Conan 90
```

{{< solution >}}
```python
class Character:
    def __init__(self, name, health):
        self.name = name
        self.health = health

    def attack_power(self):
        raise NotImplementedError("subclasses must implement attack_power()")


class Warrior(Character):
    def attack_power(self):
        return 15


class Mage(Character):
    STRONG_FROM = 50

    def attack_power(self):
        if self.health > Mage.STRONG_FROM:
            return 20
        return 10


def attack(attacker, target):
    power = attacker.attack_power()
    target.health = target.health - power
    print(attacker.name, "hits", target.name, "for", power)


conan = Warrior("Conan", 100)
merlin = Mage("Merlin", 60)
attack(conan, merlin)
attack(merlin, conan)
print(merlin.name, merlin.health)
print(conan.name, conan.health)
```

```text
Conan hits Merlin for 15
Merlin hits Conan for 10
Merlin 45
Conan 90
```

אחרי שקונאן תקף, למרלין נשארו 45 חיים — מתחת ל-50 — ולכן הכוח שלו ירד ל-10. `attack` לא יודעת כלום על הכללים של כל דמות.
{{< /solution >}}

### תרגיל 4: Duck Typing

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

**הקוד:**

```text
class Duck:
    def speak(self):
        return "Quack"

class Robot:
    def speak(self):
        return "Beep"

def make_it_speak(thing):
    return thing.speak()
```

**המשימה:**
1. `Duck` ו-`Robot` לא יורשים מאותו Parent. האם `make_it_speak` תעבוד עם שניהם? למה?
2. הסבירו במשפט אחד מה זה Duck Typing.

{{< solution >}}
1. **כן.** `make_it_speak` רק קוראת ל-`speak()` — היא לא בודקת את הסוג. כל אובייקט שיש לו `speak` מתאים.
2. **Duck Typing**: "אם זה הולך כמו ברווז ומגעגע כמו ברווז — זה ברווז". ב-Python מה שקובע הוא **שהמתודה קיימת**, לא מאיזה Class האובייקט. Class בסיס משותף עדיין שימושי — כדי להגדיר את הממשק במקום אחד ולתת התנהגות ברירת מחדל — אבל הוא לא חובה.
{{< /solution >}}

### תרגיל 5: NotImplementedError

**מה בונים:** צורות שכל אחת יודעת לחשב את השטח שלה — ו-Parent שמחייב את זה.

**הקוד:**

```text
class Shape:
    def area(self):
        raise NotImplementedError("subclasses must implement area()")

class Square(Shape):
    def __init__(self, side):
        self.side = side
```

**המשימה:**
1. מה יקרה בקריאה לשטח של `Square(3)`? למה זה דווקא טוב?
2. תקנו את `Square` כך שתחשב שטח.
3. הוסיפו `Circle` שמקבלת רדיוס, ומחשבת שטח עם `math.pi`, מעוגל לשתי ספרות.
4. הדפיסו בלולאה את השטח של ריבוע עם צלע 3 ועיגול עם רדיוס 3.

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

```text
9
28.27
```

{{< solution >}}
1. תיזרק `NotImplementedError: subclasses must implement area()`. זה **טוב**: הטעות מתגלה מיד ובבירור — מי שכתב את `Square` שכח לממש את `area`. בלי זה הייתה מתקבלת תוצאה שגויה בשקט.
2.

```python
import math


class Shape:
    def area(self):
        raise NotImplementedError("subclasses must implement area()")


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

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


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return round(math.pi * self.radius ** 2, 2)


for shape in [Square(3), Circle(3)]:
    print(shape.area())
```

```text
9
28.27
```
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **Polymorphism** = אותה קריאה, התנהגות שונה לפי סוג האובייקט. הקוד הקורא לא צריך לדעת את הסוג.
- בונים **ממשק משותף** ב-Parent (עם `raise NotImplementedError` למתודות חובה), וכל Child מממשת בדרכה.
- **מחליף** שרשראות `if isinstance` / `if type ==`. סוג חדש = Class חדשה, בלי לשנות קוד קיים.
- מתודה ב-Parent שקוראת `self.x()` מגיעה לגרסה של ה-Child (Template Method).
- **Duck Typing**: ב-Python מספיק שלאובייקט יש את המתודה. ירושה משותפת עדיין מומלצת כתיעוד.
- כל המימושים מקבלים **אותם פרמטרים**.

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

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

- [Python Glossary — duck-typing](https://docs.python.org/3/glossary.html#term-duck-typing) — ממשק משותף בלי לבדוק טיפוס
- [Python Tutorial — Inheritance](https://docs.python.org/3/tutorial/classes.html#inheritance) — Methods שנדרסות במחלקות-בת

<!-- docs:end -->
