Polymorphism: ממשק משותף, התנהגות שונה
- להסביר מהו Polymorphism במילים פשוטות ובדוגמה
- לכתוב קוד שעובד על אובייקטים מסוגים שונים דרך ממשק משותף, בלי לבדוק את הסוג
- להבין למה Polymorphism מחליף שרשראות של if/elif לפי סוג
- להכיר את Duck Typing — Polymorphism בלי ירושה משותפת
מה נלמד?
- מהו Polymorphism (רב-צורתיות).
- ממשק משותף (Common Interface): אותה מתודה, התנהגות שונה לכל סוג.
- למה זה מחליף את ה-
if type == .... - Duck Typing: הגרסה הגמישה של Python.
למה צריך את זה?
נניח שמערכת תשלומים צריכה לחייב לקוח, והלקוח יכול לשלם בכרטיס אשראי, ב-PayPal או בהעברה בנקאית. בלי Polymorphism:
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)) ← התנהגות שונה, לפי סוג האובייקט. הקוד שקורא לא צריך לדעת איזה סוג יש לו ביד.
לשלט יש כפתור “הפעלה”. לוחצים עליו מול טלוויזיה, והיא נדלקת. מול מזגן, הוא מתחיל לקרר. מול רמקול, הוא מתחיל לנגן. אותו כפתור (ממשק), פעולה שונה לכל מכשיר. ואתם, שמחזיקים את השלט, לא צריכים לדעת איך כל מכשיר עובד מבפנים. מגיע מכשיר חדש? אם הוא “מבין” את הכפתור, השלט עובד איתו בלי שום שינוי.
איך זה עובד: ממשק משותף
- מגדירים ב-Parent את הממשק: אילו מתודות יש לכל הסוגים (
pay,refund,fee). - כל Child דורסת (Override) את המתודות בדרך שלה.
- הקוד שמשתמש עובד מול הממשק:
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 משותף היא הדרך המומלצת כשמתכננים ממשק במכוון: היא מתעדת מה הממשק, ומאפשרת קוד משותף.
תחביר
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/elifraise NotImplementedError ב-Parent = “כל Child חייבת לממש את המתודה הזו”. אם Child שכחה, מקבלים שגיאה ברורה ברגע הקריאה.
דוגמת קוד ראשונה
מערכת התשלומים, עם Polymorphism. week-3/payments.py:
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)פלט:
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.
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)Paid 101 with Apple Pay on iPhone 15 | fee: 1checkout לא השתנתה בכלל. הוספנו Class, והיא עובדת. בגרסת ה-if/elif היינו צריכים לערוך כל פונקציה שמטפלת בתשלומים.
Duck Typing — אובייקט שלא יורש מ-PaymentMethod:
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)Redeemed 100 from gift card GIFT-2026 | fee: 0GiftCard לא יורשת מ-PaymentMethod, אבל יש לה pay ו-fee, ולכן checkout עובדת איתה. זה Duck Typing. (בפרויקט אמיתי עדיף בכל זאת שתירש, כדי שהממשק יהיה מתועד ומפורש.)
עוד דוגמה מוכרת: הודעות
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")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.
def checkout(method, amount):
if isinstance(method, CreditCard):
...
elif isinstance(method, PayPal):
...זה מחזיר אותנו בדיוק לבעיה המקורית, רק עם Classes. אם מוצאים את עצמכם כותבים if isinstance(...) על Classes שלכם כדי להחליט איך לבצע פעולה, זה סימן שהפעולה צריכה להיות מתודה שכל Class מממשת.
הטעות 2: לשכוח לממש מתודה מופשטת.
class Cash(PaymentMethod):
pass
checkout(Cash("Rina"), 50)NotImplementedError: subclasses must implement pay()וזה טוב: השגיאה ברורה ומצביעה בדיוק על מה שחסר. בלי ה-raise ב-Parent, הייתה מתקבלת התנהגות שקטה ושגויה, או None במקום מחרוזת.
הטעות 3: ממשק לא אחיד בין הסוגים.
class CreditCard(PaymentMethod):
def pay(self, amount, cvv):
...עכשיו checkout (שקוראת method.pay(total)) נכשלת רק על כרטיסי אשראי. כל המימושים צריכים לקבל את אותם פרמטרים. מידע שספציפי לסוג (כמו CVV) שייך ל-__init__ או ל-Attribute של אותו סוג.
הטעות 4: לחשוב ש-Polymorphism מחייב ירושה. ב-Python לא (Duck Typing). אבל ירושה מ-Base משותף מתעדת את הממשק, ומונעת טעויות. העדיפו אותה כשמתכננים.
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
מה מתרגלים בדף הזה: פולימורפיזם — אותה קריאה לכמה סוגים, כשכל סוג מתנהג אחרת; הוספת סוג חדש בלי לשנות קוד קיים; Duck Typing ו-NotImplementedError.
בסוף הדף תדעו:
- להחליף שרשרת
ifלפי סוג במתודה משותפת שכל סוג מממש - להוסיף סוג חדש בלי לגעת בקוד שמשתמש בו
- להסביר מה זה Duck Typing
- לחייב Children לממש מתודה בעזרת
NotImplementedError
תרגיל 1: במקום שרשרת if
מה בונים: אמצעי תשלום בקופה — בלי לבדוק בקוד מה הסוג של כל אחד.
הקוד שמחליפים:
def pay(method, amount):
if method["type"] == "credit":
return "Charging card " + str(amount)
elif method["type"] == "paypal":
return "Redirecting to PayPal " + str(amount)מה לכתוב:
- Class בסיס בשם
PaymentMethod, עםpayשמקבלת סכום וזורקתNotImplementedError. - שני Children:
CreditCardו-PayPal. כל אחד דורס אתpayומחזיר את המשפט שלו מהפלט. - פונקציה בשם
checkoutשמקבלת אמצעי תשלום וסכום, וקוראת ל-pay— בליifעל הסוג.
קוד לבדיקה:
for method in [CreditCard(), PayPal()]:
print(checkout(method, 100))הפלט הרצוי:
Charging card 100
Redirecting to PayPal 100הצג פתרון
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))Charging card 100
Redirecting to PayPal 100checkout לא יודעת ולא צריכה לדעת איזה סוג תשלום זה — היא רק קוראת ל-pay. כל סוג מממש אותה אחרת. זה Polymorphism.
תרגיל 2: סוג חדש — בלי לגעת ב-checkout
מה בונים: ממשיכים את הקופה — מוסיפים אמצעי תשלום חדש ועמלה.
מה לכתוב:
- Child חדש בשם
ApplePay, עםpayשמחזירה את המשפט שלו מהפלט. - ב-
PaymentMethodמוסיפיםfeeשמקבלת סכום ומחזירה 0 — ברירת מחדל לכולם. - ב-
CreditCardדורסים אתfee: עמלה של 2% מהסכום. - מעדכנים את
checkoutכך שתחזיר גם את העמלה, כמו בפלט.
קוד לבדיקה:
for method in [CreditCard(), PayPal(), ApplePay()]:
print(checkout(method, 100))הפלט הרצוי:
Charging card 100 | fee: 2.0
Redirecting to PayPal 100 | fee: 0
Paid with Apple Pay 100 | fee: 0הצג פתרון
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))Charging card 100 | fee: 2.0
Redirecting to PayPal 100 | fee: 0
Paid with Apple Pay 100 | fee: 0ApplePay נוסף בלי לשנות אף שורה ב-checkout. PayPal ו-ApplePay יורשים את fee של ה-Parent, ו-CreditCard דורס אותה.
תרגיל 3: דמויות במשחק
מה בונים: קרב בין דמויות במשחק — כשלכל סוג דמות כוח התקפה אחר.
מה לכתוב:
- Class בסיס בשם
Character, עם שם וחיים, ו-attack_powerשזורקתNotImplementedError. Warrior— כוח ההתקפה שלו תמיד 15.Mage— כוח ההתקפה שלו 10, ועוד 10 אם יש לו יותר מ-50 חיים.- פונקציה בשם
attackשמקבלת תוקף ונתקף: מורידה לנתקף חיים לפי כוח התוקף, ומדפיסה משפט כמו בפלט.
קוד לבדיקה:
conan = Warrior("Conan", 100)
merlin = Mage("Merlin", 60)
attack(conan, merlin)
attack(merlin, conan)
print(merlin.name, merlin.health)
print(conan.name, conan.health)הפלט הרצוי:
Conan hits Merlin for 15
Merlin hits Conan for 10
Merlin 45
Conan 90הצג פתרון
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)Conan hits Merlin for 15
Merlin hits Conan for 10
Merlin 45
Conan 90אחרי שקונאן תקף, למרלין נשארו 45 חיים — מתחת ל-50 — ולכן הכוח שלו ירד ל-10. attack לא יודעת כלום על הכללים של כל דמות.
תרגיל 4: Duck Typing
מה עושים: קוראים קוד ומסבירים — בלי לכתוב קוד.
הקוד:
class Duck:
def speak(self):
return "Quack"
class Robot:
def speak(self):
return "Beep"
def make_it_speak(thing):
return thing.speak()המשימה:
Duckו-Robotלא יורשים מאותו Parent. האםmake_it_speakתעבוד עם שניהם? למה?- הסבירו במשפט אחד מה זה Duck Typing.
הצג פתרון
- כן.
make_it_speakרק קוראת ל-speak()— היא לא בודקת את הסוג. כל אובייקט שיש לוspeakמתאים. - Duck Typing: “אם זה הולך כמו ברווז ומגעגע כמו ברווז — זה ברווז”. ב-Python מה שקובע הוא שהמתודה קיימת, לא מאיזה Class האובייקט. Class בסיס משותף עדיין שימושי — כדי להגדיר את הממשק במקום אחד ולתת התנהגות ברירת מחדל — אבל הוא לא חובה.
תרגיל 5: NotImplementedError
מה בונים: צורות שכל אחת יודעת לחשב את השטח שלה — ו-Parent שמחייב את זה.
הקוד:
class Shape:
def area(self):
raise NotImplementedError("subclasses must implement area()")
class Square(Shape):
def __init__(self, side):
self.side = sideהמשימה:
- מה יקרה בקריאה לשטח של
Square(3)? למה זה דווקא טוב? - תקנו את
Squareכך שתחשב שטח. - הוסיפו
Circleשמקבלת רדיוס, ומחשבת שטח עםmath.pi, מעוגל לשתי ספרות. - הדפיסו בלולאה את השטח של ריבוע עם צלע 3 ועיגול עם רדיוס 3.
הפלט הרצוי:
9
28.27הצג פתרון
- תיזרק
NotImplementedError: subclasses must implement area(). זה טוב: הטעות מתגלה מיד ובבירור — מי שכתב אתSquareשכח לממש אתarea. בלי זה הייתה מתקבלת תוצאה שגויה בשקט.
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())9
28.27סיכום
- Polymorphism = אותה קריאה, התנהגות שונה לפי סוג האובייקט. הקוד הקורא לא צריך לדעת את הסוג.
- בונים ממשק משותף ב-Parent (עם
raise NotImplementedErrorלמתודות חובה), וכל Child מממשת בדרכה. - מחליף שרשראות
if isinstance/if type ==. סוג חדש = Class חדשה, בלי לשנות קוד קיים. - מתודה ב-Parent שקוראת
self.x()מגיעה לגרסה של ה-Child (Template Method). - Duck Typing: ב-Python מספיק שלאובייקט יש את המתודה. ירושה משותפת עדיין מומלצת כתיעוד.
- כל המימושים מקבלים אותם פרמטרים.
דוקומנטציה רשמית
- Python Glossary — duck-typing — ממשק משותף בלי לבדוק טיפוס
- Python Tutorial — Inheritance — Methods שנדרסות במחלקות-בת