OOP חלק ב׳ — Inheritance ו-Design Patterns
OOP חלק ב׳ — Inheritance, Polymorphism ועיצוב מערכת
בשיעור 7 למדת לבנות Class בודד. בשיעור הזה לומדים לבנות ארכיטקטורה — כיצד Classes מדברים אחד עם השני, מורישים ומוחלפים. זה ההבדל בין "אני יודע Python" לבין "אני יודע לעצב תוכנה".
- Inheritance — class Child(Parent), super().__init__(), Override
- MRO — סדר חיפוש methods, Diamond Problem, C3 linearization
- Abstract Classes — ABC, @abstractmethod, חובת מימוש
- Polymorphism — אותו interface, מימושים שונים, Duck Typing
- Composition vs Inheritance — IS-A vs HAS-A, "Favor Composition"
- Singleton Pattern — instance אחד בלבד, __new__
- Factory Pattern — הסתרת יצירה, Open/Closed Principle
- Observer Pattern — Subject/Observer, subscribe/notify
- עיצוב מערכת — שיטת 5 צעדים כמו בסדנה
Inheritance: ירושה
Inheritance מאפשרת ל-Class לרשת attributes ו-methods מ-Class אחר. כלל ה-IS-A: Dog IS-A Animal → ירושה הגיונית. Dog IS-A Vehicle → לא הגיוני!
class Animal:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
self._energy = 100
def eat(self, food: str):
self._energy += 10
print(f"{self.name} אוכל {food}")
def describe(self) -> str:
return f"{self.name} (גיל {self.age})"
def __str__(self): return self.describe()
class Dog(Animal): # ← Dog יורש מ-Animal
def __init__(self, name: str, age: int, breed: str):
super().__init__(name, age) # ← חובה! קורא ל-__init__ של Animal
self.breed = breed
def bark(self):
print(f"{self.name}: וואוו!")
def describe(self) -> str: # ← Override!
base = super().describe() # ← קורא לגרסה של Parent
return f"{base}, גזע: {self.breed}"
class Cat(Animal):
def __init__(self, name, age, indoor=True):
super().__init__(name, age)
self.indoor = indoor
def meow(self): print(f"{self.name}: מיאו!")
def describe(self) -> str:
base = super().describe()
return f"{base}, {'פנימי' if self.indoor else 'חיצוני'}"
rex = Dog("Rex", 3, "Golden Retriever")
luna = Cat("Luna", 2, indoor=True)
print(rex) # Rex (גיל 3), גזע: Golden Retriever
rex.eat("עצם") # Rex אוכל עצם ← מ-Animal!
rex.bark() # Rex: וואוו!
print(isinstance(rex, Dog)) # True
print(isinstance(rex, Animal)) # True ← ירושה!
print(issubclass(Dog, Animal)) # Truesuper() — גישה ל-Parent
class Vehicle:
def __init__(self, brand, speed):
self.brand, self.speed = brand, speed
class Car(Vehicle):
def __init__(self, brand, speed, doors):
super().__init__(brand, speed) # חובה!
self.doors = doors
class ElectricCar(Car):
def __init__(self, brand, speed, doors, battery):
super().__init__(brand, speed, doors)
self.battery = battery
tesla = ElectricCar("Tesla", 250, 4, 100)
print(tesla.brand, tesla.speed, tesla.doors, tesla.battery)
# Tesla 250 4 100class A:
def greet(self): return "A"
class B(A):
def greet(self): return "B"
class C(A):
def greet(self): return "C"
class D(B, C): pass
print(D().greet())מה יחזיר? (MRO)Abstract Classes: חוזה שחובה לממש
Abstract Class הוא class שלא ניתן ליצור ממנו instances ישירות. הוא מגדיר Abstract Methods — מתודות שכל תת-מחלקה חייבת לממש. זהו "חוזה" בין ה-Base לבין Children.
from abc import ABC, abstractmethod
import math
class Shape(ABC): # ABC = Abstract Base Class
def __init__(self, color: str = "שחור"):
self.color = color
@abstractmethod
def area(self) -> float:
"""חישוב שטח — חייב להיות מממש ב-subclass"""
pass
@abstractmethod
def perimeter(self) -> float:
pass
# Method רגילה — נורש לכולם, לא חובה לoverride
def describe(self) -> str:
return (f"{type(self).__name__} {self.color}: "
f"שטח={self.area():.2f}, היקף={self.perimeter():.2f}")
def __str__(self): return self.describe()
class Circle(Shape):
def __init__(self, radius, color="שחור"):
super().__init__(color)
self.radius = radius
def area(self): return math.pi * self.radius ** 2
def perimeter(self): return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height, color="שחור"):
super().__init__(color)
self.width, self.height = width, height
def area(self): return self.width * self.height
def perimeter(self): return 2 * (self.width + self.height)
shapes = [Circle(5, "אדום"), Rectangle(4, 6, "כחול")]
for shape in shapes:
print(shape)
# s = Shape() # TypeError! Cannot instantiate abstract class! שכחת לממש abstract method ›
class BadShape(Shape):
def area(self):
return 42
# שכחנו perimeter()!
b = BadShape()
# TypeError: Can't instantiate abstract class BadShape
# with abstract method perimeter
# אם אפילו method אחת abstract לא מומשה — TypeError!from abc import ABC, abstractmethod; class Shape(ABC): @abstractmethod def area(self): pass; s = Shape()Composition vs Inheritance
Composition = "HAS-A". במקום לרשת, מחזיקים אובייקט כ-attribute. הכלל: "Favor Composition over Inheritance".
| קריטריון | Inheritance | Composition |
|---|---|---|
| קשר | IS-A (Dog IS-A Animal) | HAS-A (Car HAS-A Engine) |
| גמישות | נמוכה — חוזה קשיח | גבוהה — ניתן להחליף |
| שימוש | שיתוף התנהגות משותפת | בנייה מרכיבים |
| בעיה | Diamond Problem, tight coupling | יותר קוד boilerplate |
# BAD: Car IS-A Engine? לא!
class BadCar(Engine): # ← לא הגיוני!
pass
# GOOD: Car HAS-A Engine (Composition)
class Engine:
def __init__(self, hp):
self.hp = hp
self._running = False
def start(self):
self._running = True
print(f"מנוע {self.hp} כ.ס. פועל")
@property
def is_running(self): return self._running
class GPS:
def navigate(self, dest): print(f"מנווט ל-{dest}")
class Car:
def __init__(self, brand, hp):
self.brand = brand
self._engine = Engine(hp) # HAS-A Engine
self._gps = GPS() # HAS-A GPS
def start(self):
self._engine.start() # delegation!
print(f"{self.brand} מוכן לנסיעה")
def navigate(self, dest):
if not self._engine.is_running:
print("אי אפשר לנווט — מנוע כבוי!")
return
self._gps.navigate(dest) # delegation!
car = Car("Toyota", 150)
car.start() # מנוע 150 כ.ס. פועל → Toyota מוכן לנסיעה
car.navigate("ירושלים") # מנווט לירושליםDesign Patterns: Singleton, Factory, Observer
Design Patterns הן פתרונות מוכחים לבעיות עיצוב נפוצות. בראיון גאמא — שאלות עליהם קלאסיות.
🏗️ Singleton Pattern
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if hasattr(self, '_initialized'): return
self._initialized = True
self.config = {}
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True! — אותו object
s1.config["key"] = "value"
print(s2.config) # {'key': 'value'} ← אותו object!🏗️ Factory Pattern
from abc import ABC, abstractmethod
class Notification(ABC):
@abstractmethod
def send(self, message, recipient): pass
class EmailNotification(Notification):
def send(self, m, r): print(f"[EMAIL] → {r}: {m}")
class SMSNotification(Notification):
def send(self, m, r): print(f"[SMS] → {r}: {m}")
class NotificationFactory:
_creators = {
"email": EmailNotification,
"sms": SMSNotification,
}
@classmethod
def create(cls, notification_type: str) -> Notification:
creator = cls._creators.get(notification_type.lower())
if creator is None:
raise ValueError(f"סוג לא מוכר: {notification_type}")
return creator()
@classmethod
def register(cls, name, creator):
cls._creators[name] = creator # Open/Closed!
# שימוש:
NotificationFactory.create("email").send("שלום!", "alice@ex.com")
NotificationFactory.create("sms").send("קוד: 1234", "+972-50")🏗️ Observer Pattern
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, event_type: str, data): pass
class Subject:
def __init__(self):
self._observers = []
def subscribe(self, observer): self._observers.append(observer)
def unsubscribe(self, observer): self._observers.remove(observer)
def notify(self, event_type, data=None):
for observer in self._observers:
observer.update(event_type, data)
class SecuritySystem(Subject):
def __init__(self):
super().__init__()
self._armed = False
def arm(self):
self._armed = True
self.notify("armed", {"time": "09:00"})
def trigger_alert(self, location):
if self._armed:
self.notify("alert", {"location": location})
class EmailAlert(Observer):
def __init__(self, email): self.email = email
def update(self, event_type, data):
if event_type == "alert":
print(f"[EMAIL→{self.email}] אזעקה: {data['location']}")
system = SecuritySystem()
system.subscribe(EmailAlert("owner@home.com"))
system.arm()
system.trigger_alert("חלון כניסה")עיצוב מערכת מלאה: כמו בסדנה
בסדנה תקבל משימה: "בנה מערכת X". הנה שיטת 5 צעדים לגשת אליה.
2. זיהוי קשרים: IS-A? HAS-A? מי יורש ממי?
3. Abstract Classes: מה משותף לכולם? → ABC
4. Polymorphism: איפה ה-behavior משתנה בין סוגים?
5. Design Patterns: יש Singleton? Factory? Observer?
from abc import ABC, abstractmethod
# דוגמה: מערכת HR — Employee, Manager, CEO
class Employee(ABC):
def __init__(self, name: str, base_salary: float):
self.name = name
self._base = base_salary
@abstractmethod
def calculate_salary(self) -> float: pass
@property
def salary(self) -> float: return self.calculate_salary()
def __str__(self):
return f"{type(self).__name__}: {self.name} — ₪{self.salary:,.0f}"
class RegularEmployee(Employee):
def calculate_salary(self): return self._base
class Manager(Employee):
def __init__(self, name, base, bonus_pct=0.2):
super().__init__(name, base)
self._bonus_pct = bonus_pct
self._team = []
def add_to_team(self, emp): self._team.append(emp)
def calculate_salary(self): return self._base * (1 + self._bonus_pct)
class CEO(Manager):
def __init__(self, name, base, bonus_pct=0.5, equity=0):
super().__init__(name, base, bonus_pct)
self._equity = equity
def calculate_salary(self): return super().calculate_salary() + self._equity
class Department:
def __init__(self, name): self.name = name; self._employees = []
def hire(self, emp): self._employees.append(emp)
def total_payroll(self): return sum(e.salary for e in self._employees)
def __str__(self): return f"מחלקת {self.name}, שכר כולל: ₪{self.total_payroll():,.0f}"
dev = Department("פיתוח")
dev.hire(RegularEmployee("Alice", 15000))
dev.hire(Manager("Carol", 20000, 0.25))
dev.hire(CEO("Dan", 50000, 0.5, 10000))
for emp in dev._employees: # Polymorphism!
print(emp)
print(dev)class Parent:
def hello(self):
print("Parent")
self.world() # ← Template Method!
def world(self): print("Parent.world")
class Child(Parent):
def world(self): print("Child.world")
Child().hello()מה הפלט?class S:
_i = None
def __new__(cls):
if cls._i is None:
cls._i = super().__new__(cls)
return cls._i
a = S(); b = S(); c = S()
print(a is b, b is c)מה הפלט?אתגר מעשי — OOP: ירושה ופולימורפיזם
שיא השיעור: לבנות, לתקן ולנתח — לא לזהות תשובה. פתרו את משימת החובה ואז את האתגר שמעליה; ההגשה נבדקת אוטומטית מול מקרי-קצה נסתרים (וחלקן אוכפות יעילות דרך גודל-קלט).
class Employee:
def __init__(self, name, salary):
self.name = name; self.salary = salary
class Manager(Employee):
def __init__(self, name, salary, team):
self.team = team # באג כאן
def summary(self):
return f"{self.name}: {self.salary}, team {self.team}"סיכום השיעור
- Inheritance: class Child(Parent), super().__init__(), Override
- MRO: סדר חיפוש methods, Diamond Problem, C3 linearization
- Abstract Classes: ABC, @abstractmethod, חובת מימוש ב-subclass
- Polymorphism: אותו interface, מימושים שונים, Duck Typing
- Composition vs Inheritance: IS-A vs HAS-A, "Favor Composition"
- Singleton: instance אחד בלבד, __new__, DB/Config/Logger
- Factory: הסתרת יצירה, dict של creators, Open/Closed Principle
- Observer: Subject/Observer, subscribe/unsubscribe/notify
- עיצוב מערכת: שיטת 5 צעדים — Entities, Relations, ABC, Poly, Patterns
לפני שיעור 9 — כתוב מערכת Employee מהראש ב-15 דקות. אם לא — חזור על שיעורים 7–8.
שאלות תרגול ותשובות (10)
שאלה 1 · מה עושה super().__init__() במחלקה יורשת?
- א. קורא לבנאי של מחלקת האב כדי לאתחל את השדות שלה
- ב. יוצר מופע חדש של מחלקת האב
- ג. מוחק את השדות של האב
- ד. מגדיר מחלקה אבסטרקטית
בלי הקריאה, האתחול של האב לא מתבצע והשדות שלו לא קיימים במופע.
שאלה 2 · מה יודפס?
class A:
def greet(self): return "A"
class B(A):
def greet(self): return "B"
print(B().greet())- א. A
- ב. B
- ג. AB
- ד. שגיאה
Overriding — המימוש של B דורס את זה של A.
שאלה 3 · מה יקרה בעת יצירת מופע של מחלקה יורשת שלא מימשה מתודה שסומנה ב-@abstractmethod?
- א. המתודה תתעלם
- ב. ייזרק TypeError בזמן היצירה
- ג. המתודה תרוץ מהאב
- ד. הקוד יתקמפל אך יחזיר None
ABC מונע יצירת מופע של מחלקה שלא מימשה את כל המתודות האבסטרקטיות.
שאלה 4 · מהו סדר ה-MRO של class D(B, C) כאשר B(A) ו-C(A)?
- א. D → B → C → A → object
- ב. D → A → B → C
- ג. D → C → B → A
- ד. אין סדר מוגדר — נזרקת שגיאה
C3 Linearization: המחלקה, ההורים משמאל לימין, ורק אז האב המשותף.
שאלה 5 · מהו החיסרון המרכזי של Duck Typing?
- א. הוא איטי יותר בביצוע
- ב. שגיאות טיפוס מתגלות רק בזמן ריצה
- ג. הוא לא מאפשר ירושה
- ד. הוא קיים רק ב-C#
אין בדיקה סטטית — חוסר מתודה יתגלה כ-AttributeError רק כשהקוד ירוץ.
שאלה 6 · מתי נעדיף Composition על פני Inheritance?
- א. כשהיחס הוא "יש-לו" (HAS-A) ולא "הוא-סוג-של" (IS-A)
- ב. תמיד, ירושה אסורה
- ג. כשיש רק מחלקה אחת
- ד. כשעובדים בפייתון בלבד
למכונית יש מנוע — היא אינה מנוע; לכן הרכבה ולא ירושה.
שאלה 7 · מדוע Singleton נחשב לעיתים Anti-Pattern?
- א. הוא איטי מדי
- ב. הוא מצב גלובלי שיוצר צימוד ומקשה על בדיקות
- ג. הוא לא נתמך בפייתון
- ד. הוא צורך יותר זיכרון מכל דפוס אחר
מצב גלובלי דולף בין טסטים ומונע החלפה ב-mock; החלופה המודרנית היא Dependency Injection.
שאלה 8 · איזה מנגנון בעולם האמיתי מממש בפועל את דפוס Observer?
- א. Garbage Collection
- ב. Webhooks
- ג. Name Mangling
- ד. Type Casting
ה-Subject שולח POST לכל ה-URLs הרשומים — בדיוק notify() לכל ה-Observers.
שאלה 9 · מה מבטיח עקרון ההחלפה של ליסקוב (Liskov Substitution)?
- א. כל מחלקה חייבת לרשת ממחלקה אחת בלבד
- ב. ניתן להחליף מופע של מחלקת אב במופע של מחלקה יורשת בלי לשבור את התוכנית
- ג. ירושה מרובה אסורה
- ד. מחלקה אבסטרקטית לא יכולה להכיל שדות
היורשת חייבת לכבד את החוזה של האב — אחרת הפולימורפיזם נשבר.
שאלה 10 · מה מבדיל @abstractmethod ממתודה רגילה בתוך ABC?
- א. היא חייבת להיות מיושמת במחלקה היורשת
- ב. היא לא יכולה לקבל פרמטרים
- ג. היא רצה אוטומטית בעת היצירה
- ד. היא פרטית
המתודה האבסטרקטית מגדירה חוזה מחייב; מתודה רגילה ב-ABC היא מימוש ברירת מחדל שניתן לרשת כמות שהוא.