אובייקטים בעבודה: מצב, הסתרת מידע, Properties ומחלקה שלמה
- לעבוד עם רשימה של אובייקטים — לולאה, חיפוש ומיון — ועם אובייקט שמחזיק אובייקט
- להסביר מה קורה כששני שמות מצביעים על אותו אובייקט
- להסתיר נתונים מאחורי ממשק ציבורי, ולהבחין בין
_nameל-__name - לאכוף כללים בכל השמה עם
@propertyו-setter, ולכתוב שדה לקריאה בלבד ושדה מחושב - לתכנן ולבנות מחלקה שלמה — מאחריות במשפט אחד, דרך כללים, ועד ממשק ברור
מה בשיעור
השיעור הזה בנוי כסיפור אחד, באותו סדר כמו במצגת. כל פרק נשען על הקודם:
- פרק ראשון — אובייקטים ומצב: הרבה אובייקטים, מצב שמשתנה, ואובייקט שמחזיק אובייקט
- פרק שני — הסתרת מידע: כל קוד יכול לשבור את הנתונים, אז מסתירים אותם מאחורי ממשק
- פרק שלישי — בדיקת תקינות: תחביר פשוט של שדה, ובדיקה בכל השמה עם
@property - פרק רביעי — מחלקה שלמה: מחברים את הכול למחלקה אחת עם אחריות, כללים וממשק
בסוף השיעור — תרגול, ומיני פרויקט שמחבר את כל הפרקים.
פרק ראשון: אובייקטים ומצב — הרבה אובייקטים ומצב שמשתנה
מה נלמד?
- איך מנהלים הרבה אובייקטים: רשימות, חיפוש, סינון ומיון.
- איך ה-State של אובייקטים משתנה לאורך התוכנית.
- אובייקט שמחזיק אובייקט אחר (למשל: הזמנה שמחזיקה לקוח).
- הפניות: שני שמות לאותו אובייקט, מול שני אובייקטים.
למה צריך את זה?
מערכת אמיתית היא לא אובייקט אחד. היא מאות אובייקטים שמדברים זה עם זה: לקוחות, הזמנות, מוצרים. צריך לדעת למצוא את הלקוח עם מזהה 7, את כל ההזמנות שלא שולמו, ולעדכן את המלאי כשהזמנה נשלחת. וגם להבין מה קורה כשמשנים אובייקט שכמה חלקים במערכת “מחזיקים”.
הסבר מהבסיס
אובייקטים ברשימה
רשימה יכולה להחזיק אובייקטים בדיוק כמו שהיא מחזיקה מספרים או dicts. כל מה שלמדנו בנושא Python עובד:
| פעולה | עם dicts (נושא Python) | עם אובייקטים |
|---|---|---|
| גישה לשדה | item["price"] | item.price |
| מיון | sorted(items, key=lambda i: i["price"]) | sorted(items, key=lambda i: i.price) |
| סינון | if item["price"] > 100 | if item.price > 100 |
| פעולה | deposit(account, 50) | account.deposit(50) |
State לאורך זמן
State = הערכים של ה-Attributes עכשיו. אובייקט “חי” לאורך התוכנית, וה-State שלו משתנה דרך מתודות: הזמנה נוצרת במצב "new", עוברת ל-"paid", ואז ל-"shipped". כל אובייקט עובר את המסלול בנפרד.
אובייקט שמחזיק אובייקט
Attribute יכול להחזיק כל ערך, כולל אובייקט אחר:
order.customer = dana # the order "knows" its customerorder.customer הוא אובייקט Customer, ולכן אפשר להמשיך: order.customer.name.
הפניות (References) — תזכורת קריטית
זוכרים מנושא Python: משתנה הוא מדבקה על אובייקט. זה נכון גם לאובייקטים שלנו:
dana ──────────────►┌──────────────┐
order1.customer ───►│ Customer │ ONE object, three names
order2.customer ───►│ name: "Dana" │
└──────────────┘אם דנה משנה כתובת (dana.city = "Tel Aviv"), שתי ההזמנות רואות את הכתובת החדשה מיד. הן לא מחזיקות עותק, הן מחזיקות הפניה לאותה דנה. זה בדרך כלל בדיוק מה שרוצים: יש דנה אחת במערכת.
כמה קבוצות וואטסאפ מכילות את “דנה”. כשדנה מחליפה תמונת פרופיל, היא מתעדכנת בכל הקבוצות, כי הקבוצות לא מחזיקות צילום של דנה. הן מחזיקות קישור לדנה האמיתית.
תחביר
objects = [ClassName(...), ClassName(...)]
for obj in objects: # loop
obj.method()
found = None
for obj in objects: # search
if obj.attr == target:
found = obj
break
matching = [obj for obj in objects if obj.attr > 10] # filter (list comprehension)
ordered = sorted(objects, key=lambda obj: obj.attr) # sort
holder.other = some_object # an attribute that holds another object
holder.other.attr # chain through itList Comprehension (שורה 3 מלמטה) היא דרך קצרה לבנות רשימה מסוננת. היא שקולה ללולאה:
matching = []
for obj in objects:
if obj.attr > 10:
matching.append(obj)השתמשו בגרסה שקריאה לכם יותר. שתיהן נכונות.
דוגמת קוד ראשונה
week-3/team.py:
class Player:
def __init__(self, name, score=0):
self.name = name
self.score = score
def add_points(self, points):
self.score = self.score + points
def __str__(self):
return self.name + ": " + str(self.score)
team = [Player("Dana"), Player("Avi"), Player("Noa")]
team[0].add_points(30)
team[1].add_points(15)
team[2].add_points(40)
team[0].add_points(20)
for player in team:
print(player)
leader = max(team, key=lambda player: player.score)
print("Leader:", leader.name)
ranking = sorted(team, key=lambda player: player.score, reverse=True)
for place, player in enumerate(ranking, start=1):
print(place, player)פלט:
Dana: 50
Avi: 15
Noa: 40
Leader: Dana
1 Dana: 50
2 Noa: 40
3 Avi: 15פירוק הקוד שורה אחר שורה
| קוד | הסבר |
|---|---|
team = [Player("Dana"), ...] | רשימה של 3 אובייקטים נפרדים, כל אחד עם score משלו. |
team[0].add_points(30) | team[0] הוא אובייקט Player. קוראים למתודה שלו. רק ה-State שלו משתנה. |
team[0].add_points(20) | אותו אובייקט שוב. ה-score שלו מצטבר ל-50. |
max(team, key=lambda p: p.score) | מוצא את האובייקט עם ה-score הגבוה, ומחזיר את האובייקט כולו. |
sorted(..., reverse=True) | רשימה חדשה ממוינת. team עצמה לא השתנתה. אבל האובייקטים הם אותם אובייקטים, ורק הסדר ברשימה החדשה שונה. |
דוגמה נוספת
לקוחות והזמנות — אובייקטים שמחזיקים אובייקטים, ו-State שמשתנה. week-3/orders.py:
class Customer:
def __init__(self, name, city):
self.name = name
self.city = city
class Order:
VALID_STATUSES = ["new", "paid", "shipped"]
next_number = 1
def __init__(self, customer, total):
self.number = Order.next_number
Order.next_number = Order.next_number + 1
self.customer = customer
self.total = total
self.status = "new"
def advance(self):
position = Order.VALID_STATUSES.index(self.status)
if position == len(Order.VALID_STATUSES) - 1:
raise ValueError("order " + str(self.number) + " is already shipped")
self.status = Order.VALID_STATUSES[position + 1]
def __str__(self):
return ("Order #" + str(self.number) + " | " + self.customer.name + " (" + self.customer.city
+ ") | " + str(self.total) + " NIS | " + self.status)
dana = Customer("Dana", "Haifa")
avi = Customer("Avi", "Eilat")
orders = [Order(dana, 120), Order(avi, 80), Order(dana, 300)]
orders[0].advance()
orders[0].advance()
orders[2].advance()
dana.city = "Tel Aviv"
for order in orders:
print(order)
dana_orders = [order for order in orders if order.customer is dana]
print("Dana's total:", sum(order.total for order in dana_orders))
not_shipped = [order.number for order in orders if order.status != "shipped"]
print("Not shipped yet:", not_shipped)פלט:
Order #1 | Dana (Tel Aviv) | 120 NIS | shipped
Order #2 | Avi (Eilat) | 80 NIS | new
Order #3 | Dana (Tel Aviv) | 300 NIS | paid
Dana's total: 420
Not shipped yet: [2, 3]פירוק:
Order(dana, 120)— ההזמנה מחזיקה הפניה לאובייקטdana. היא לא מעתיקה את השם ואת העיר.self.customer.name— שרשור: מההזמנה ללקוח, ומשם לשם הלקוח.dana.city = "Tel Aviv"— שינוי אחד, ושתי ההזמנות של דנה מראות את העיר החדשה. אילו שמרנוself.city = customer.cityב-__init__, ההזמנות היו “תקועות” עם חיפה.advance()— State Machine פשוטה: new → paid → shipped, ואי אפשר לדלג או לחזור. כל הזמנה מתקדמת בנפרד.order.customer is dana—isכאן נכון: אנחנו באמת שואלים “האם זה אותו אובייקט?” (ולא “האם יש לו אותו שם?”, כי יכולות להיות שתי לקוחות בשם דנה).sum(order.total for order in dana_orders)— סכום בשורה אחת (Generator Expression, כמו ב-maxשל נושא Python).
סיכום
- אובייקטים ברשימה עובדים כמו כל ערך: לולאה, חיפוש,
sorted/maxעםkey=lambda obj: obj.attr. - כל אובייקט מחזיק State משלו, שמשתנה דרך המתודות שלו.
- Attribute יכול להחזיק אובייקט אחר (
order.customer), וניגשים בשרשור:order.customer.name. - זו הפניה, לא עותק. שינוי באובייקט נראה מכל מי שמחזיק אותו.
b = a= שם נוסף, לא אובייקט חדש.isבודק “אותו אובייקט”.- שינוי State “מבחוץ” עוקף את הכללים. בפרק הבא: Encapsulation.
דוקומנטציה רשמית
- Instance Objects — אובייקט ומצב משלו
פרק שני: הסתרת מידע — Encapsulation
מה נלמד?
- מהי Encapsulation (כימוס) ולמה היא קיימת.
- ממשק ציבורי (Public Interface) מול מימוש פנימי (Implementation).
- המוסכמות של Python:
_protectedו-__private. - מה זה Name Mangling.
למה צריך את זה?
עד עכשיו כתבנו מתודות עם בדיקות יפות, למשל withdraw שבודקת שיש מספיק כסף. אבל:
account = BankAccount("Dana", 100)
account.balance = -1_000_000שורה אחת עקפה את כל הכללים. אף שגיאה, אף לוג. ה-withdraw המושקע פשוט לא רץ. במערכת אמיתית, עם עשרות מפתחים ומאות קבצים, מישהו יכתוב שורה כזו (אולי אתם, בעוד חודש, “רק לבדיקה”).
עוד בעיה: נניח שהחלטנו לשמור את היתרה באגורות (balance_cents) כדי למנוע בעיות עיגול. אם 30 מקומות בקוד ניגשים ישירות ל-account.balance, כולם נשברים. אילו הם היו משתמשים במתודות, היינו משנים רק את המתודות.
Encapsulation פותרת את שתי הבעיות.
הסבר מהבסיס
מהי Encapsulation?
Encapsulation = אריזה של הנתונים יחד עם הקוד שמטפל בהם, והסתרת הפרטים הפנימיים, כך שהעולם החיצוני עובד רק דרך ממשק מוגדר.
שני רעיונות:
- הגנה על כללים (Invariants) — נתון רגיש משתנה רק דרך מתודות שבודקות שהוא נשאר חוקי. “יתרה לא שלילית”, “ציון בין 0 ל-100”, “סטטוס מתקדם רק קדימה”.
- הסתרת מימוש — מי שמשתמש ב-Class צריך לדעת מה היא עושה, לא איך. כך אפשר לשנות את ה"איך" בלי לשבור את מי שמשתמש בה.
Public Interface מול Implementation
| ממשק ציבורי (Public Interface) | מימוש פנימי (Implementation) | |
|---|---|---|
| מה | המתודות וה-Attributes שמיועדים לשימוש מבחוץ | הנתונים והמתודות הפנימיות שמאפשרים לממשק לעבוד |
| דוגמה בחשבון בנק | deposit(), withdraw(), get_balance() | איך היתרה נשמרת, רשימת התנועות, בדיקות עזר |
| יציבות | משתנה לעיתים רחוקות. אחרים תלויים בו | מותר לשנות בחופשיות |
לכספומט יש ממשק ציבורי: מסך, מקשים, חריץ לכרטיס, פתח לשטרות. אתם יכולים “למשוך 200”, והכספומט בודק קוד, יתרה ומגבלה יומית. הכספת בפנים (המימוש) נעולה. אם כל אחד היה יכול לפתוח את הכספת ולקחת כסף ישירות, כל הבדיקות היו חסרות משמעות. והבנק יכול להחליף את מנגנון הספירה בפנים בלי שתשימו לב, כי הממשק נשאר אותו דבר.
איך Python עושה את זה: מוסכמות
בשפות כמו Java יש “נעילה” אמיתית (private). ב-Python אין נעילה מוחלטת. יש מוסכמות (Conventions) שכל מפתחי Python מכבדים:
| כתיבה | שם | המשמעות | מה Python עושה |
|---|---|---|---|
name | Public | חלק מהממשק. מותר להשתמש מבחוץ | כלום |
_name | Protected (מוסכמה) | “פנימי, אל תיגע מבחוץ” | כלום. זו רק הודעה למפתחים. VS Code גם לא יציע אותו בהשלמה אוטומטית |
__name | Private (Name Mangling) | “פנימי מאוד” | משנה את השם מאחורי הקלעים, כדי שגישה מבחוץ “בטעות” תיכשל |
הפילוסופיה של Python: “We are all consenting adults here”. השפה סומכת עליכם. קו תחתון אומר “זה לא בשבילך”. מי שבכל זאת נוגע, לוקח אחריות.
Name Mangling
כשכותבים self.__balance בתוך Class בשם BankAccount, Python משנה את השם בפועל ל-_BankAccount__balance. לכן account.__balance מבחוץ נכשל ב-AttributeError. זה לא אבטחה (אפשר עדיין לגשת דרך account._BankAccount__balance), אבל זה מונע גישה בטעות, ומבהיר מאוד את הכוונה.
בקורס הזה: נשתמש בעיקר ב-_name (קו אחד), שהיא הדרך המקובלת ביותר בקוד Python אמיתי. את __name נכיר כדי לדעת מה הוא עושה כשנראה אותו.
תחביר
class ClassName:
def __init__(self, value):
self.public_value = value # public: part of the interface
self._internal = [] # protected by convention: "don't touch from outside"
self.__secret = 0 # private: name-mangled to _ClassName__secret
def public_method(self): # public interface
self._helper()
return self.__secret
def _helper(self): # internal helper method
...דוגמת קוד ראשונה
week-3/encapsulated_account.py:
class BankAccount:
def __init__(self, owner, balance=0):
if balance < 0:
raise ValueError("initial balance cannot be negative")
self.owner = owner
self._balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive")
self._balance = self._balance + amount
def withdraw(self, amount):
if amount <= 0:
raise ValueError("withdrawal must be positive")
if amount > self._balance:
raise ValueError("insufficient funds")
self._balance = self._balance - amount
def get_balance(self):
return self._balance
account = BankAccount("Dana", 100)
account.deposit(50)
account.withdraw(30)
print("Balance:", account.get_balance())
try:
account.withdraw(1000)
except ValueError as error:
print("Rejected:", error)
print("Balance still:", account.get_balance())פלט:
Balance: 120
Rejected: insufficient funds
Balance still: 120פירוק הקוד שורה אחר שורה
| קוד | הסבר |
|---|---|
self.owner | Public. מותר לקרוא, ואין כלל מיוחד שצריך להגן עליו. |
self._balance | Protected (מוסכמה). הקו התחתון אומר לכל מפתח: “את היתרה לא משנים ישירות”. |
deposit / withdraw | הממשק הציבורי לשינוי: הדרך היחידה המכובדת לשנות יתרה. כל שינוי עובר בדיקה. |
get_balance() | Getter: מתודה שמחזירה את הערך לקריאה. מבחוץ אפשר לקרוא את היתרה, אבל לא לשנות אותה ישירות. |
account.withdraw(1000) נכשל | והיתרה לא השתנתה. הכלל נשמר. |
ומה אם מישהו בכל זאת כותב account._balance = -999? Python לא תעצור אותו. אבל הקו התחתון הופך את זה לטעות ברורה וגלויה בקוד: כל מי שעושה Code Review יראה מיד “נוגעים ב-_balance מבחוץ — אסור”. זו בדיוק המטרה.
דוגמה נוספת
__private ו-Name Mangling בפעולה, ומתודת עזר פנימית. week-3/secure_vault.py:
class Vault:
MAX_ATTEMPTS = 3
def __init__(self, code):
self.__code = code
self._failed_attempts = 0
self.is_locked = False
def open(self, code):
if self.is_locked:
return "Vault is locked"
if self._check_code(code):
self._failed_attempts = 0
return "Vault opened"
self._failed_attempts = self._failed_attempts + 1
if self._failed_attempts >= Vault.MAX_ATTEMPTS:
self.is_locked = True
return "Wrong code - vault is now LOCKED"
return "Wrong code (" + str(self._failed_attempts) + "/" + str(Vault.MAX_ATTEMPTS) + ")"
def _check_code(self, code):
return code == self.__code
vault = Vault("1234")
print(vault.open("0000"))
print(vault.open("1234"))
try:
print(vault.__code)
except AttributeError as error:
print("AttributeError:", error)
print("Mangled name:", vault._Vault__code)פלט:
Wrong code (1/3)
Vault opened
AttributeError: 'Vault' object has no attribute '__code'
Mangled name: 1234פירוק:
self.__code— בתוך ה-Class כותבים__code, ו-Python שומרת אותו בפועל כ-_Vault__code.vault.__codeמבחוץ ←AttributeError. לא ניתן לגשת “בטעות”.vault._Vault__code← עובד! Name Mangling הוא לא אבטחה. סיסמה אמיתית לעולם לא שומרים בקוד (ובטח שלא בטקסט גלוי). כאן זו רק דוגמה להמחשה._check_code— מתודת עזר פנימית. היא חלק מהמימוש, ולא מהממשק. מחר אפשר להחליף אותה בבדיקה מוצפנת, ושום קוד חיצוני לא יישבר, כי אף אחד מבחוץ לא אמור לקרוא לה.is_locked— Public, לקריאה. (מבחוץ אפשר גם לכתוב אליו, וזו “חור” קטן. בפרק הבא נראה איך הופכים Attribute לקריאה בלבד עם@property.)
סיכום
- Encapsulation = נתונים + הקוד שמטפל בהם יחד, וחשיפה של ממשק בלבד.
- למה: (1) הגנה על כללים: שינוי רק דרך מתודות שבודקות. (2) הסתרת מימוש: אפשר לשנות “איך” בלי לשבור את מי שמשתמש.
name= Public._name= “פנימי, אל תיגע” (מוסכמה, הנפוצה ביותר).__name= Name Mangling (_Class__name).- Python לא נועלת. המוסכמות מבהירות כוונה ומונעות טעויות. זו לא אבטחה.
- להחזיר עותק של list/dict פנימי, ולא את המקור.
- לא מסתירים הכול. מסתירים כשיש כלל או פרט מימוש.
דוקומנטציה רשמית
- Private Variables — _name ו-__name
פרק שלישי: בדיקת תקינות — Getters, Setters ו-@property
מה נלמד?
- מה הם Getter ו-Setter.
- Validation (אימות): בדיקת כל ערך חדש לפני שהוא נשמר.
@property: הדרך של Python לעשות את זה בצורה אלגנטית.- Attribute לקריאה בלבד ו-Attribute מחושב.
למה צריך את זה?
בפרק הקודם הגנו על _balance, כי היו לו מתודות טבעיות (deposit, withdraw). אבל מה עם נתון שכן אמור להשתנות ישירות, רק בתוך כללים?
- גיל של משתמש: אפשר לעדכן, אבל לא ל-(-5) ולא ל-300.
- מחיר של מוצר: אפשר לשנות, אבל לא לשלילי.
- אימייל: אפשר לעדכן, אבל רק לכתובת עם
@.
אם age הוא Attribute רגיל, כל אחד יכול לכתוב user.age = -5. אם מסתירים אותו (_age), צריך דרך מסודרת לקרוא ולשנות אותו. זה Getter ו-Setter.
הסבר מהבסיס
Getter ו-Setter — הרעיון
- Getter — מתודה שמחזירה את הערך של נתון פנימי.
get_age(). - Setter — מתודה שמקבלת ערך חדש, בודקת אותו, ורק אם הוא חוקי, שומרת.
set_age(new_age).
כך ל-Class יש שליטה מלאה: כל קריאה וכל כתיבה עוברות דרכה.
לא ניגשים לבד לתיקייה שלכם בארכיון הבנק ומשנים כתובת עם עט. פונים לפקיד (Setter): הוא בודק שהכתובת תקינה ושאתם מורשים, ורק אז מעדכן. ומי שרוצה לראות את הכתובת, מבקש מהפקיד (Getter). הוא יכול להחליט מה להראות, ואולי להסתיר חלק מהפרטים.
הבעיה עם get_ ו-set_ ב-Python
זה עובד, אבל זה מסורבל:
user.set_age(user.get_age() + 1)לעומת:
user.age = user.age + 1ובנוסף, אם בהתחלה age היה Attribute רגיל ו-50 מקומות בקוד כותבים user.age, ואחר כך מחליטים שצריך ולידציה, צריך לשנות את כל 50 המקומות ל-get_/set_.
@property — הפתרון של Python
@property מאפשר להגדיר מתודה שנראית מבחוץ כמו Attribute רגיל:
- כשכותבים
user.age, נקראת מתודת ה-Getter. - כשכותבים
user.age = 30, נקראת מתודת ה-Setter, עם הוולידציה.
הקוד שמשתמש ב-Class כותב user.age, כרגיל, אבל מאחורי הקלעים כל גישה עוברת דרך הבדיקות. הטוב משני העולמות.
@property הוא Decorator (קשטן): שורה שמתחילה ב-@ מעל def, ו"עוטפת" את הפונקציה בהתנהגות נוספת. לא נעמיק ב-Decorators. מספיק לדעת את התבנית של @property. (בנושא Backend ומסדי נתונים, FastAPI ישתמש ב-Decorators כמו @app.get(...).)
תחביר
גרסה 1 — מתודות Getter/Setter קלאסיות (הרעיון):
class ClassName:
def __init__(self, value):
self._value = None
self.set_value(value) # validation also on creation
def get_value(self):
return self._value
def set_value(self, new_value):
if not valid(new_value):
raise ValueError("...")
self._value = new_valueגרסה 2 — @property (הדרך המקובלת ב-Python):
class ClassName:
def __init__(self, value):
self.value = value # goes through the setter below!
@property
def value(self): # getter: runs on obj.value
return self._value
@value.setter
def value(self, new_value): # setter: runs on obj.value = x
if not valid(new_value):
raise ValueError("...")
self._value = new_value- שתי המתודות נקראות בשם זהה (
value), כשם ה-“Attribute” הציבורי. - הנתון האמיתי נשמר בשם אחר, עם קו תחתון (
_value). @propertyמעל ה-Getter.@value.setter(שם ה-property +.setter) מעל ה-Setter.- Read-only: מגדירים רק
@property, בלי Setter.
דוגמת קוד ראשונה
הרעיון — Getter ו-Setter קלאסיים. week-3/user_classic.py:
class User:
MIN_AGE = 0
MAX_AGE = 120
def __init__(self, name, age):
self.name = name
self._age = None
self.set_age(age)
def get_age(self):
return self._age
def set_age(self, new_age):
if not isinstance(new_age, int):
raise TypeError("age must be an int, got " + type(new_age).__name__)
if new_age < User.MIN_AGE or new_age > User.MAX_AGE:
raise ValueError("age must be between 0 and 120, got " + str(new_age))
self._age = new_age
dana = User("Dana", 24)
dana.set_age(dana.get_age() + 1)
print(dana.name, dana.get_age())
for bad_value in [-5, "thirty"]:
try:
dana.set_age(bad_value)
except (ValueError, TypeError) as error:
print("Rejected:", error)
print("Age unchanged:", dana.get_age())פלט:
Dana 25
Rejected: age must be between 0 and 120, got -5
Rejected: age must be an int, got str
Age unchanged: 25פירוק הקוד שורה אחר שורה
| קוד | הסבר |
|---|---|
self._age = None ואז self.set_age(age) | גם ביצירה, הגיל עובר דרך ה-Setter, ולכן אי אפשר ליצור משתמש עם גיל לא חוקי. ולידציה במקום אחד. |
get_age() | Getter: מחזיר את הערך הפנימי. |
set_age(new_age) | Setter: קודם בודק טיפוס (isinstance), אחר כך טווח, ורק אז שומר. |
type(new_age).__name__ | שם הטיפוס כטקסט נקי ("str"), במקום <class 'str'>. |
dana.set_age(dana.get_age() + 1) | עובד, אבל מסורבל. מכאן המוטיבציה ל-@property. |
Age unchanged: 25 | הערכים הלא חוקיים לא נשמרו. |
דוגמה נוספת
אותו רעיון עם @property, ועוד Attribute לקריאה בלבד ו-Attribute מחושב. week-3/product_property.py:
class Product:
def __init__(self, sku, name, price):
self._sku = sku
self.name = name
self.price = price
@property
def sku(self):
return self._sku
@property
def price(self):
return self._price
@price.setter
def price(self, new_price):
if not isinstance(new_price, (int, float)):
raise TypeError("price must be a number")
if new_price <= 0:
raise ValueError("price must be positive, got " + str(new_price))
self._price = new_price
@property
def price_with_vat(self):
return round(self._price * 1.17, 2)
laptop = Product("A1", "Laptop", 3000)
print(laptop.sku, laptop.name, laptop.price, laptop.price_with_vat)
laptop.price = laptop.price * 0.9
print("After discount:", laptop.price, laptop.price_with_vat)
try:
laptop.price = -100
except ValueError as error:
print("Rejected:", error)
try:
laptop.sku = "HACKED"
except AttributeError as error:
print("Rejected:", error)
try:
Product("A2", "Ghost", 0)
except ValueError as error:
print("Cannot create:", error)פלט:
A1 Laptop 3000 3510.0
After discount: 2700.0 3159.0
Rejected: price must be positive, got -100
Rejected: property 'sku' of 'Product' object has no setter
Cannot create: price must be positive, got 0פירוק:
laptop.price— נראה כמו Attribute, אבל בפועל מריץ את מתודת ה-Getter. אין סוגריים!laptop.price = laptop.price * 0.9— תחביר טבעי לגמרי, אבל עובר דרך ה-Setter עם הבדיקות.self.price = priceב-__init__— גם זה עובר דרך ה-Setter. לכןProduct("A2", "Ghost", 0)נכשל כבר ביצירה. ולידציה אחת לשני המקרים.sku—@propertyבלי Setter ← קריאה בלבד. מק"ט לא אמור להשתנות אחרי היצירה. ניסיון לשנות זורקAttributeError.price_with_vat— Property מחושב: אין לו נתון משלו, והוא מחושב בכל גישה מ-_price. תמיד מעודכן (זוכרים אתdescriptionשהתיישן? הנה פתרון אלגנטי עוד יותר).name— נשאר Attribute רגיל, כי אין עליו כלל. לא מוסיפים property בלי סיבה.
סיכום
- Getter מחזיר נתון פנימי. Setter מקבל ערך, בודק, ושומר. כך ל-Class יש שליטה על כל קריאה וכתיבה.
- ב-Python משתמשים ב-
@property(Getter) וב-@name.setter(Setter): מבחוץ זה נראה כמו Attribute רגיל. - הנתון האמיתי נשמר בשם עם קו תחתון (
_price). אותו שם יוצר רקורסיה. - ב-
__init__כותביםself.price = price, כדי שגם היצירה תעבור ולידציה. - property בלי Setter = קריאה בלבד. property מחושב = תמיד מעודכן.
- לא מוסיפים property בלי כלל. מתחילים עם Attribute רגיל, ומשדרגים כשצריך.
דוקומנטציה רשמית
- property() — @property ו-setter
פרק רביעי: מחלקה שלמה — אחריות, כללים וממשק
מה נלמד?
עד עכשיו למדנו חלקים: __init__, מתודות, Class Attributes, Encapsulation ו-properties. היום נחבר את כולם לתהליך עבודה אחד: איך בונים Class שלמה, טובה, מאפס.
למה צריך את זה?
הקושי האמיתי ב-OOP הוא לא התחביר, אלא ההחלטות: מה ה-Class אחראית לו? מה חושפים? מה מסתירים? אילו כללים היא חייבת לשמור? מתחילים שקופצים ישר לקוד מקבלים Class שעושה הכול, חושפת הכול, ושוברת כללים. תהליך קבוע מונע את זה.
הסבר מהבסיס
התהליך — 6 שלבים
| שלב | שאלה | תוצר |
|---|---|---|
| 1. אחריות | מה ה-Class הזו עושה, במשפט אחד? | משפט |
| 2. נתונים | מה היא צריכה לזכור? | רשימת Attributes |
| 3. ממשק ציבורי | מה העולם החיצוני צריך לעשות איתה ולדעת עליה? | רשימת מתודות ו-properties |
| 4. כללים (Invariants) | מה חייב להישאר נכון תמיד? | רשימת כללים |
| 5. מימוש | איך שומרים את הנתונים בפנים? אילו מתודות עזר? | קוד |
| 6. בדיקה | האם כל כלל נשמר? מה עם מקרי קצה? | סקריפט בדיקה |
Invariant (שמורה) = תנאי שחייב להיות נכון בכל רגע בחיי האובייקט. למשל “יתרה ≥ 0”, או “מספר הרשומים ≤ מספר המקומות”. כל מתודה שמשנה State צריכה להשאיר את כל ה-Invariants נכונים.
“משפט אחד” הוא מבחן ל-SRP: אם המשפט צריך “וגם… וגם…”, כנראה שאלה שתי Classes.
תחביר
אין תחביר חדש. נשלב את כל מה שלמדנו:
class Name:
CLASS_CONSTANT = ... # shared rules / constants
def __init__(self, ...): # validation through properties
...
@property # read access / read-only / computed
def value(self): ...
def command(self, ...): # changes state, keeps invariants
...
def query(self): ... # returns info, changes nothing
def _helper(self): ... # internal
def __str__(self): ... # human-readableדוגמת קוד ראשונה
המשימה: מערכת הרשמה לקורסים. נבנה Class בשם Course לפי 6 השלבים.
שלב 1 — אחריות: “Course מנהלת את ההרשמה לקורס אחד: מי רשום, כמה מקומות נשארו, ורשימת ההמתנה.”
שלב 2 — נתונים: קוד קורס, שם, מספר מקומות מקסימלי, רשימת רשומים, רשימת המתנה.
שלב 3 — ממשק ציבורי:
| חבר בממשק | סוג | תיאור |
|---|---|---|
code, title, capacity | properties (קריאה בלבד) | פרטי הקורס |
enroll(student) | Setter | רושם. אם מלא, מכניס לרשימת המתנה. מחזיר "enrolled" או "waitlisted" |
drop(student) | Setter | מבטל הרשמה. הראשון ברשימת ההמתנה נכנס במקומו |
is_enrolled(student) | Getter | |
available_seats | property מחושב | |
is_full | property מחושב | |
roster() | Getter | עותק של רשימת הרשומים |
__str__ | הצגה |
שלב 4 — כללים:
- מספר הרשומים ≤
capacity, תמיד. - סטודנט לא רשום פעמיים, ולא נמצא גם ברשומים וגם בהמתנה.
capacityהוא מספר שלם חיובי.- כשמתפנה מקום ויש ממתינים, הראשון בתור נכנס (FIFO, First In First Out).
שלב 5 — מימוש. week-3/course.py:
class Course:
MAX_CAPACITY = 500
def __init__(self, code, title, capacity):
if not isinstance(capacity, int) or capacity <= 0 or capacity > Course.MAX_CAPACITY:
raise ValueError("capacity must be a whole number between 1 and " + str(Course.MAX_CAPACITY))
self._code = code
self._title = title
self._capacity = capacity
self._enrolled = []
self._waitlist = []
@property
def code(self):
return self._code
@property
def title(self):
return self._title
@property
def capacity(self):
return self._capacity
@property
def available_seats(self):
return self._capacity - len(self._enrolled)
@property
def is_full(self):
return self.available_seats == 0
def is_enrolled(self, student):
return student in self._enrolled
def enroll(self, student):
self._ensure_not_registered(student)
if self.is_full:
self._waitlist.append(student)
return "waitlisted"
self._enrolled.append(student)
return "enrolled"
def drop(self, student):
if student in self._waitlist:
self._waitlist.remove(student)
return
if student not in self._enrolled:
raise ValueError(student + " is not registered to " + self._code)
self._enrolled.remove(student)
if self._waitlist:
next_student = self._waitlist.pop(0)
self._enrolled.append(next_student)
def roster(self):
return list(self._enrolled)
def waitlist(self):
return list(self._waitlist)
def _ensure_not_registered(self, student):
if student in self._enrolled or student in self._waitlist:
raise ValueError(student + " is already registered to " + self._code)
def __str__(self):
return (self._code + " " + self._title + " | " + str(len(self._enrolled)) + "/"
+ str(self._capacity) + " enrolled | waitlist: " + str(len(self._waitlist)))פירוק הקוד שורה אחר שורה
| חלק | איך הוא משרת את התכנון |
|---|---|
ולידציה ב-__init__ | כלל 3: אי אפשר ליצור קורס עם capacity לא חוקי. |
_enrolled, _waitlist (פנימיים) | Encapsulation: אף אחד מבחוץ לא יכול לעשות append ולעקוף את כלל 1. |
code, title, capacity כ-properties בלי Setter | קריאה בלבד: פרטי הקורס לא משתנים אחרי היצירה. |
available_seats, is_full מחושבים | אין נתון כפול שעלול לצאת מסנכרון. תמיד נגזרים מ-_enrolled. |
enroll | כלל 2 (דרך _ensure_not_registered), וכלל 1 (בדיקת is_full לפני הכנסה). |
drop | כלל 4: pop(0) = הראשון שנכנס לתור יוצא ראשון. |
roster() / waitlist() מחזירות עותק | מי שקיבל רשימה לא יכול לשנות את הפנימית. |
_ensure_not_registered | מתודת עזר פנימית: כלל אחד, במקום אחד. |
__str__ | תמונת מצב קריאה, לדיבאג ולהדפסה. |
דוגמה נוספת
שלב 6 — בדיקה. סקריפט שבודק כל כלל, כולל מקרי קצה. בסוף course.py:
def main():
course = Course("PY101", "Intro to Python", 2)
print(course.enroll("Dana"))
print(course.enroll("Avi"))
print(course.enroll("Noa"))
print(course.enroll("Tom"))
print(course)
try:
course.enroll("Dana")
except ValueError as error:
print("Rule 2 holds:", error)
course.drop("Avi")
print("After Avi dropped:", course.roster(), "| waitlist:", course.waitlist())
leaked = course.roster()
leaked.append("Hacker")
print("Roster after outside append:", course.roster())
try:
course.drop("Ghost")
except ValueError as error:
print("Drop unknown:", error)
try:
Course("BAD1", "Broken", 0)
except ValueError as error:
print("Rule 3 holds:", error)
print("Seats:", course.available_seats, "| full?", course.is_full)
if __name__ == "__main__":
main()פלט:
enrolled
enrolled
waitlisted
waitlisted
PY101 Intro to Python | 2/2 enrolled | waitlist: 2
Rule 2 holds: Dana is already registered to PY101
After Avi dropped: ['Dana', 'Noa'] | waitlist: ['Tom']
Roster after outside append: ['Dana', 'Noa']
Drop unknown: Ghost is not registered to PY101
Rule 3 holds: capacity must be a whole number between 1 and 500
Seats: 0 | full? Trueמה הבדיקה הוכיחה:
- כלל 1: אף פעם לא יותר מ-2 רשומים. השלישי והרביעי נכנסו להמתנה.
- כלל 2: דנה לא נרשמה פעמיים.
- כלל 4: כשאבי עזב, נועה (הראשונה בתור) נכנסה, וטום נשאר בהמתנה.
- Encapsulation:
appendעל העותק לא שינה את הרשימה האמיתית. - כלל 3: קורס עם 0 מקומות לא נוצר.
סיכום
- תהליך: אחריות (משפט אחד) ← נתונים ← ממשק ציבורי ← כללים (Invariants) ← מימוש ← בדיקה.
- Invariant = מה שחייב להיות נכון תמיד. כל Setter חייב לשמור על כולם.
- נתונים פנימיים מוסתרים (
_x). פרטים קבועים הם properties לקריאה בלבד. מידע נגזר הוא properties מחושבים. - החזרת עותקים של אוספים פנימיים. כלל אחד ← מתודת עזר אחת (
_ensure_...). - בודקים כל כלל ומקרי קצה, לא רק את המסלול השמח.
דוקומנטציה רשמית
- Random Remarks — מוסכמות לכתיבת מחלקות
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
מה מתרגלים בדף הזה: רשימה של אובייקטים, אובייקט שמחזיק אובייקט אחר, הסתרת נתונים (Encapsulation), property עם בדיקה, ומחלקה שלמה עם כללים.
בסוף הדף תדעו:
- לעבור על רשימה של אובייקטים ולמצוא בה לפי תכונה
- לבנות אובייקט שמחזיק הפניה לאובייקט אחר
- להסתיר נתון מאחורי מתודות שבודקות כל שינוי
- לכתוב property עם setter שאוכף כלל
- לבנות מחלקה שלמה שאוכפת את הכללים שלה
תרגיל 1: רשימה של שחקנים
מה בונים: טבלת שחקנים במשחק — ומוצאים את המוביל.
מה לכתוב:
- Class בשם
Player. ה-__init__מקבל שם, והניקוד מתחיל מ-0. add_points— מקבלת מספר נקודות ומוסיפה לניקוד.__str__— מחזירה שם וניקוד, למשלDana: 50.- אחרי קוד הבדיקה: עוברים בלולאה על הרשימה, מוצאים את השחקן עם הניקוד הגבוה ביותר, ומדפיסים אותו.
קוד לבדיקה:
players = [Player("Dana"), Player("Avi"), Player("Noa")]
players[0].add_points(50)
players[1].add_points(15)
players[2].add_points(40)הפלט הרצוי:
Leader: Dana: 50הצג פתרון
class Player:
def __init__(self, name):
self.name = name
self.score = 0
def add_points(self, points):
self.score = self.score + points
def __str__(self):
return self.name + ": " + str(self.score)
players = [Player("Dana"), Player("Avi"), Player("Noa")]
players[0].add_points(50)
players[1].add_points(15)
players[2].add_points(40)
leader = players[0]
for player in players:
if player.score > leader.score:
leader = player
print("Leader:", leader)Leader: Dana: 50תרגיל 2: אובייקט שמחזיק אובייקט
מה בונים: לקוח והזמנות — כשכמה הזמנות מחזיקות את אותו לקוח.
מה לכתוב:
- Class בשם
Customerעם שם ועיר. - Class בשם
Orderעם לקוח (אובייקטCustomer) וסכום. - יוצרים לקוח אחד, ושתי הזמנות שמקבלות את אותו אובייקט לקוח.
- מדפיסים את העיר דרך שתי ההזמנות, משנים את העיר של הלקוח לתל אביב, ומדפיסים שוב.
הפלט הרצוי:
Haifa Haifa
Tel Aviv Tel Avivמה מבינים מזה: ההזמנה לא מחזיקה עותק של הלקוח, אלא הפניה אליו.
הצג פתרון
class Customer:
def __init__(self, name, city):
self.name = name
self.city = city
class Order:
def __init__(self, customer, total):
self.customer = customer
self.total = total
dana = Customer("Dana", "Haifa")
order1 = Order(dana, 120)
order2 = Order(dana, 80)
print(order1.customer.city, order2.customer.city)
dana.city = "Tel Aviv"
print(order1.customer.city, order2.customer.city)Haifa Haifa
Tel Aviv Tel Avivההזמנות לא מחזיקות עותק של הלקוח, אלא הפניה אליו. שינוי בלקוח נראה מכל הזמנה שמחזיקה אותו.
תרגיל 3: BankAccount מוגן
מה בונים: חשבון בנק שאי אפשר לשבור — היתרה משתנה רק דרך מתודות שבודקות אותה.
מה לכתוב:
- Class בשם
BankAccount, עם בעלים ויתרה. את היתרה שומרים ב-_balance— קו תחתון אומר “פנימי, לא לגעת מבחוץ”. deposit— מוסיפה סכום.withdraw— מורידה סכום. אם אין מספיק כסף:ValueErrorעם ההודעה שבפלט.get_balance— מחזירה את היתרה.
קוד לבדיקה:
account = BankAccount("Dana", 100)
account.deposit(50)
try:
account.withdraw(500)
except ValueError as error:
print("error:", error)
print(account.get_balance())הפלט הרצוי:
error: not enough money
150הצג פתרון
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
self._balance = self._balance + amount
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("not enough money")
self._balance = self._balance - amount
def get_balance(self):
return self._balance
account = BankAccount("Dana", 100)
account.deposit(50)
try:
account.withdraw(500)
except ValueError as error:
print("error:", error)
print(account.get_balance())error: not enough money
150תרגיל 4: Setter שבודק
מה בונים: משתמש שהגיל שלו תמיד חוקי — עם property שבודק כל השמה.
מה לכתוב:
- Class בשם
User, ש-__init__שלה מקבלת גיל ושומרת אותו דרך ה-property. - Getter עם
@propertyבשםage— מחזיר את הגיל שנשמר ב-_age. - Setter עם
@age.setter— בודק שהגיל בין 0 ל-120. אם לא:ValueErrorעם ההודעה שבפלט. - שימו לב: גם ההשמה ב-
__init__עוברת דרך הבדיקה.
קוד לבדיקה:
user = User(30)
user.age = user.age + 1
print(user.age)
try:
user.age = -5
except ValueError as error:
print("error:", error)
print(user.age)הפלט הרצוי:
31
error: age must be 0-120
31הצג פתרון
class User:
def __init__(self, age):
self.age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0 or value > 120:
raise ValueError("age must be 0-120")
self._age = value
user = User(30)
user.age = user.age + 1
print(user.age)
try:
user.age = -5
except ValueError as error:
print("error:", error)
print(user.age)31
error: age must be 0-120
31self.age = ageב-__init__עובר דרך ה-setter — אז גם אובייקט חדש נבדק.- הערך שמור ב-
_age, וה-property חושף אותו כאילו הוא Attribute רגיל.
תרגיל 5: ההרשמה
מה בונים: הרשמה לקורס עם מספר מקומות מוגבל ורשימת המתנה.
מה לכתוב:
- Class בשם
Course, עם קוד קורס, שם וקיבולת, ושתי רשימות פנימיות: רשומים וממתינים. enroll— מקבלת שם של סטודנט:- אם הוא כבר רשום או ממתין:
ValueErrorעם ההודעה שבפלט; - אם יש מקום: מוסיפה לרשומים ומחזירה
"enrolled"; - אחרת: מוסיפה לממתינים ומחזירה
"waitlisted".
- אם הוא כבר רשום או ממתין:
__str__— קוד, שם, כמה רשומים מתוך הקיבולת, וכמה ממתינים.
קוד לבדיקה:
course = Course("PY101", "Intro to Python", 2)
for student in ["Dana", "Avi", "Noa"]:
print(student, course.enroll(student))
print(course)
try:
course.enroll("Dana")
except ValueError as error:
print("error:", error)הפלט הרצוי:
Dana enrolled
Avi enrolled
Noa waitlisted
PY101 Intro to Python | 2/2 enrolled | waitlist: 1
error: Dana is already registeredהצג פתרון
class Course:
def __init__(self, code, name, capacity):
if capacity <= 0:
raise ValueError("capacity must be positive")
self.code = code
self.name = name
self.capacity = capacity
self._enrolled = []
self._waitlist = []
def enroll(self, student):
if student in self._enrolled or student in self._waitlist:
raise ValueError(student + " is already registered")
if len(self._enrolled) < self.capacity:
self._enrolled.append(student)
return "enrolled"
self._waitlist.append(student)
return "waitlisted"
def __str__(self):
return (self.code + " " + self.name + " | " + str(len(self._enrolled)) + "/"
+ str(self.capacity) + " enrolled | waitlist: " + str(len(self._waitlist)))
course = Course("PY101", "Intro to Python", 2)
for student in ["Dana", "Avi", "Noa"]:
print(student, course.enroll(student))
print(course)
try:
course.enroll("Dana")
except ValueError as error:
print("error:", error)Dana enrolled
Avi enrolled
Noa waitlisted
PY101 Intro to Python | 2/2 enrolled | waitlist: 1
error: Dana is already registeredמיני פרויקט: מלאי של חנות
איזו מערכת בונים: מערכת מלאי לחנות קטנה. לכל מוצר יש שם, מחיר וכמות במלאי. אפשר למכור ולהוסיף מלאי, והמערכת שומרת שלא יקרה דבר לא חוקי — מחיר שלילי, מכירה של יותר ממה שיש, או שני מוצרים עם אותו שם. בסוף היא מציגה את המוצרים לפי שווי, ואת שווי המלאי כולו.
מה תלמדו מהפרויקט:
- להסתיר נתונים (Encapsulation): לשנות אותם רק דרך מתודות שבודקות
- להשתמש ב-property: עם setter שבודק, לקריאה בלבד, ומחושב
- לבנות Class שמחזיקה רשימה של אובייקטים אחרים
- למיין אובייקטים לפי ערך מחושב
מה צריך לבנות — שלב אחרי שלב:
חלק א — מוצר (Product)
שלב 1 — יצירה. ה-__init__ מקבל שם, מחיר וכמות (ברירת מחדל 0). השם נשמר ב-_name, הכמות ב-_quantity, והמחיר עובר דרך ה-setter של price.
שלב 2 — המחיר. price הוא property עם setter שבודק שהמחיר גדול מ-0. אחרת: ValueError.
שלב 3 — שדות לקריאה בלבד. name ו-quantity הם properties בלי setter — מבחוץ אפשר לקרוא אותם, אבל לא לשנות.
שלב 4 — שינוי הכמות. משנים את הכמות רק דרך שתי מתודות:
add_stock— מוסיפה כמות. אם הכמות לא חיובית:ValueError.sell— מורידה כמות. אם אין מספיק במלאי:ValueErrorעם שם המוצר.
שלב 5 — ערך מחושב והצגה. total_value הוא property מחושב: מחיר כפול כמות. __str__ מציגה את המוצר כמו בפלט.
חלק ב — מלאי (Inventory)
שלב 6 — רשימה פנימית. Inventory מחזיקה רשימה פנימית בשם _products.
שלב 7 — הוספה וחיפוש. add מוסיפה מוצר. אם כבר יש מוצר עם אותו שם: ValueError. find מקבלת שם ומחזירה את המוצר, או None.
שלב 8 — דוחות. by_value מחזירה עותק של רשימת המוצרים, ממוין לפי total_value מהגבוה לנמוך. total_value של המלאי הוא property מחושב: סכום השווי של כל המוצרים.
הודעות השגיאה — בדיוק כך:
price must be positive
amount must be positive
not enough <product name>
<product name> already existsקוד לבדיקה:
shop = Inventory()
shop.add(Product("Pen", 5, 10))
shop.add(Product("Bag", 60, 2))
shop.add(Product("Notebook", 12, 5))
shop.find("Pen").sell(4)
shop.find("Bag").add_stock(1)
try:
shop.find("Notebook").sell(50)
except ValueError as error:
print("error:", error)
try:
shop.add(Product("Pen", 7))
except ValueError as error:
print("error:", error)
try:
shop.find("Bag").price = -3
except ValueError as error:
print("error:", error)
for product in shop.by_value():
print(product)
print("Total:", shop.total_value)הפלט הרצוי:
error: not enough Notebook
error: Pen already exists
error: price must be positive
Bag: 3 x 60 = 180
Notebook: 5 x 12 = 60
Pen: 6 x 5 = 30
Total: 270בסוף הפרויקט תדעו:
- לבנות אובייקט שמגן על הנתונים שלו — מבחוץ אי אפשר לשבור אותו
- לבחור בין שדה רגיל, property עם בדיקה, שדה לקריאה בלבד וערך מחושב
- לבנות “מנהל” שמחזיק אובייקטים, מחפש ביניהם ומסכם אותם
בונוס: הוסיפו ל-Inventory מתודה low_stock שמקבלת גבול, ומחזירה את שמות המוצרים שהכמות שלהם קטנה ממנו.
הצג פתרון
class Product:
def __init__(self, name, price, quantity=0):
self._name = name
self.price = price
self._quantity = quantity
@property
def name(self):
return self._name
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value <= 0:
raise ValueError("price must be positive")
self._price = value
@property
def quantity(self):
return self._quantity
def add_stock(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
self._quantity = self._quantity + amount
def sell(self, amount):
if amount > self._quantity:
raise ValueError("not enough " + self._name)
self._quantity = self._quantity - amount
@property
def total_value(self):
return self._price * self._quantity
def __str__(self):
return (self._name + ": " + str(self._quantity) + " x " + str(self._price)
+ " = " + str(self.total_value))
class Inventory:
def __init__(self):
self._products = []
def add(self, product):
if self.find(product.name) is not None:
raise ValueError(product.name + " already exists")
self._products.append(product)
def find(self, name):
for product in self._products:
if product.name == name:
return product
return None
def by_value(self):
return sorted(self._products, key=lambda p: p.total_value, reverse=True)
@property
def total_value(self):
total = 0
for product in self._products:
total = total + product.total_value
return total
shop = Inventory()
shop.add(Product("Pen", 5, 10))
shop.add(Product("Bag", 60, 2))
shop.add(Product("Notebook", 12, 5))
shop.find("Pen").sell(4)
shop.find("Bag").add_stock(1)
try:
shop.find("Notebook").sell(50)
except ValueError as error:
print("error:", error)
try:
shop.add(Product("Pen", 7))
except ValueError as error:
print("error:", error)
try:
shop.find("Bag").price = -3
except ValueError as error:
print("error:", error)
for product in shop.by_value():
print(product)
print("Total:", shop.total_value)error: not enough Notebook
error: Pen already exists
error: price must be positive
Bag: 3 x 60 = 180
Notebook: 5 x 12 = 60
Pen: 6 x 5 = 30
Total: 270Inventoryמחזיקה אובייקטים שלProduct(אובייקט שמחזיק אובייקטים), ו-by_valueמחזירה רשימה חדשה —sortedלא נוגע ברשימה הפנימית._name,_price,_quantityמוסתרים. מבחוץ משנים כמות רק דרךadd_stockו-sell— שבודקות את הכללים.priceעם setter שבודק — גם ב-__init__.nameו-quantityלקריאה בלבד, ו-total_valueמחושב בכל קריאה.
בונוס:
def low_stock(self, limit):
names = []
for product in self._products:
if product.quantity < limit:
names.append(product.name)
return names(מתודה בתוך Inventory.) עם הנתונים למעלה, shop.low_stock(4) מחזירה ['Bag'].