בעמוד הזה
- מה בשיעור
- פרק ראשון: למה צריך OOP — קוד פרוצדורלי מול אובייקטים
- פרק שני: Class ו-Object — מגדירים תבנית ויוצרים ממנה אובייקטים
- פרק שלישי:
__init__ו-self— אובייקט שנולד שלם - פרק רביעי: Methods — הפעולות של האובייקט
- פרק חמישי: נתונים משותפים — Instance Attribute מול Class Attribute
- פרק שישי: מה הופך ל-Class — מדרישה מילולית למחלקות
- פרק שביעי: חוזרים לבנק — הכול ביחד
- תרגול
- מיני פרויקט: ספרייה קטנה
מחלקות ואובייקטים: מהבעיה ועד Class שלמה
- לזהות את הבעיות של קוד פרוצדורלי כשהמערכת גדלה, ולהסביר איך OOP פותר אותן
- להגדיר Class וליצור ממנה כמה אובייקטים, ולהבחין בין Class ל-Object
- לכתוב
__init__שמאתחל אובייקט שלם ובודק את הערכים, ולהסביר מה זהself - להוסיף Methods — Getters ו-Setters — ו-
__str__שמציג את האובייקט - להבחין בין Instance Attribute ל-Class Attribute, ולהשתמש במונה משותף
- להפוך תיאור מילולי למבנה מחלקות — מה Class, מה Attribute ומה Method
- לבנות Class שלמה שמחברת את כל הרעיונות — במיני פרויקט
מה בשיעור
השיעור הזה בנוי כסיפור אחד, באותו סדר כמו במצגת. מתחילים ממערכת בנק קטנה שכתובה עם מילונים ופונקציות, רואים איפה היא נשברת, ובכל פרק מוסיפים כלי אחד שפותר בעיה אחת:
- פרק ראשון — למה צריך OOP: הבעיות של קוד פרוצדורלי
- פרק שני — Class ו-Object: תבנית ומופעים
- פרק שלישי —
__init__ו-self: אובייקט שנולד שלם - פרק רביעי — Methods: האובייקט לא רק יודע, הוא גם עושה
- פרק חמישי — נתונים משותפים: Class Attributes
- פרק שישי — מה הופך ל-Class: מדרישה מילולית למחלקות
- פרק שביעי — חוזרים לבנק: הכול ביחד
בסוף השיעור — תרגול, ומיני פרויקט שמחבר את כל הפרקים.
פרק ראשון: למה צריך OOP — קוד פרוצדורלי מול אובייקטים
מה נלמד?
השבוע כולו מוקדש ל-OOP — Object-Oriented Programming (תכנות מונחה עצמים). לפני שנכתוב שורת קוד אחת של OOP, נענה על השאלה החשובה ביותר: למה?
בפרק הזה:
- נכתוב מערכת קטנה בלי OOP, בדרך שאנחנו כבר מכירים.
- נראה איפה היא מתחילה להיות בעייתית.
- נבין את הרעיון של OOP כפתרון, עדיין בלי תחביר.
למה צריך את זה?
כל מה שלמדנו עד עכשיו עובד. אפשר לכתוב כל תוכנה עם משתנים, dicts, רשימות ופונקציות. זה נקרא תכנות פרוצדורלי (Procedural Programming): נתונים מצד אחד, פונקציות שפועלות עליהם מצד שני.
אבל כשמערכת גדלה, לעשרות סוגי “דברים” ומאות פונקציות, מתחילות בעיות. OOP הוא הדרך שבה כמעט כל המערכות הגדולות בנויות. וגם הכלים שתשתמשו בהם (FastAPI, ספריות Python) כתובים ב-OOP. כדי לקרוא ולהשתמש בהם, צריך להבין אותו.
הסבר מהבסיס
הבעיה: מערכת חשבונות בנק — בלי OOP
נבנה מערכת קטנה לניהול חשבונות בנק, עם מה שאנחנו כבר יודעים:
account1 = {"owner": "Dana", "balance": 1000}
account2 = {"owner": "Avi", "balance": 500}
def deposit(account, amount):
if amount <= 0:
raise ValueError("amount must be positive")
account["balance"] = account["balance"] + amount
def withdraw(account, amount):
if amount > account["balance"]:
raise ValueError("insufficient funds")
account["balance"] = account["balance"] - amount
deposit(account1, 200)
withdraw(account2, 100)
print(account1, account2){'owner': 'Dana', 'balance': 1200} {'owner': 'Avi', 'balance': 400}עובד! אז מה הבעיה? בואו נגדיל את המערכת.
בעיה 1: כל אחד יכול “לגעת” בנתונים
account1["balance"] = -999999
account2["balnce"] = 100- השורה הראשונה עוקפת את כל הבדיקות שכתבנו ב-
withdraw. יתרה שלילית, בלי שום בדיקה. - בשורה השנייה יש שגיאת כתיב. נוצר מפתח חדש
"balnce", ואין שום שגיאה (ראינו את זה בנושא מבני נתונים ופונקציות).
הכללים (לא למשוך יותר ממה שיש, סכום חיובי) קיימים רק אם כולם זוכרים להשתמש בפונקציות. שום דבר לא מחייב אותם.
בעיה 2: הנתונים והפעולות מפוזרים
כל פעולה על חשבון היא פונקציה נפרדת שמקבלת את ה-dict: deposit(account, ...), withdraw(account, ...), transfer(...), monthly_fee(...), print_statement(...). במערכת אמיתית, אלה עשרות פונקציות בכמה קבצים. מי שמקבל dict של חשבון לא יודע אילו פעולות קיימות עליו ואיפה הן.
בעיה 3: אין “מבנה” מוגדר
איך נראה חשבון? מה חייב להיות בו? אולי במקום אחד בקוד יצרו {"owner": ..., "balance": ...}, ובמקום אחר {"name": ..., "money": ...}. אין מקום אחד שמגדיר “זה חשבון בנק, אלה השדות שלו, ואלה הפעולות שלו”.
בעיה 4: סוגים דומים אבל שונים
נוסף “חשבון חיסכון”: כמו חשבון רגיל, אבל עם ריבית, ובלי אפשרות למשוך מתחת ל-1,000. עכשיו withdraw צריכה לבדוק: if account["type"] == "savings": ... elif .... ועם כל סוג חשבון חדש, כל הפונקציות מתמלאות ב-if-ים.
הרעיון של OOP
OOP מארגן את הקוד סביב “דברים” (אובייקטים), ולא סביב פעולות. כל “דבר” מכיל יחד:
- את הנתונים שלו: Attributes (תכונות). לחשבון: בעלים, יתרה.
- את הפעולות שאפשר לעשות איתו: Methods (מתודות). לחשבון: הפקדה, משיכה.
PROCEDURAL OBJECT-ORIENTED
data: account dict ┌──────────────────────────┐
functions: deposit(account, x) │ BankAccount │
withdraw(account, x) │ data: owner, balance│
print_statement(account) │ actions: deposit() │
│ withdraw() │
"functions act ON data" │ statement() │
└──────────────────────────┘
"the object knows its own data
and its own actions"ואיך זה פותר את הבעיות?
| בעיה | הפתרון של OOP | נלמד ב… |
|---|---|---|
| כל אחד נוגע בנתונים | האובייקט שומר על הנתונים שלו, ושינוי עובר דרך הפעולות שלו (Encapsulation) | נושא OOP – Classes ואובייקטים |
| נתונים ופעולות מפוזרים | הכול בתוך ה-Class. account. + Tab ב-VS Code מראה את כל מה שאפשר לעשות | היום |
| אין מבנה מוגדר | Class אחת מגדירה בדיוק אילו נתונים יש לכל חשבון | היום |
| סוגים דומים | Inheritance ו-Polymorphism: חשבון חיסכון “יורש” מחשבון רגיל ומשנה רק את מה ששונה | נושא ירושה ופולימורפיזם |
תבנית מול דבר ספציפי
הרעיון הכי בסיסי ב-OOP קיים כבר בעולם האמיתי:
| תבנית (Class) | דברים ספציפיים שנוצרו ממנה (Objects) |
|---|---|
| תוכנית בנייה של דירה | הדירה של דנה בקומה 3, הדירה של אבי בקומה 5 |
| טופס “פרטי סטודנט” | הטופס שדנה מילאה, הטופס שאבי מילא |
| תבנית לייצור מכוניות בקו ייצור | המכונית האדומה עם מספר רישוי 12-345-67 |
| מושג “חשבון בנק” | החשבון של דנה עם 1,200 ש"ח, החשבון של אבי עם 400 ש"ח |
- לכל הדירות שנבנו מאותה תוכנית יש אותו מבנה (חדרים, מטבח), אבל ערכים שונים (צבע קירות, מי גר בה).
- שינוי בדירה של דנה לא משנה את הדירה של אבי.
- התוכנית עצמה היא לא דירה. אי אפשר לגור בתוכנית.
Class = תבנית לייצור מכוניות. היא מגדירה שלכל מכונית יש צבע, דגם ומספר רישוי (Attributes), ושכל מכונית יודעת לנסוע, לבלום ולצפור (Methods). Object = מכונית אחת ספציפית שיוצרה מתוך התבנית: אדומה, דגם 2024, מספר 12-345-67. אפשר לייצר ממנה אלפי מכוניות, וכל אחת עם צבע ומספר משלה.
תחביר
עדיין לא כותבים class. בפרק הבא נלמד את התחביר המלא. אבל הנה “טעימה”, כדי לראות לאן אנחנו הולכים. אותה מערכת בנק, ב-OOP:
class BankAccount:
def __init__(self, owner, balance):
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("insufficient funds")
self.balance = self.balance - amount
account1 = BankAccount("Dana", 1000)
account1.deposit(200)
print(account1.balance)אל תנסו להבין כל מילה עכשיו (class, __init__, self), כי על כל אחת מהן יש פרק שלם בהמשך השיעור. רק שימו לב לדבר אחד: הנתונים (owner, balance) והפעולות (deposit, withdraw) נמצאים באותו מקום, וקוראים להן דרך האובייקט: account1.deposit(200).
דוגמת קוד ראשונה
כבר השתמשתם באובייקטים כל הזמן, בלי לדעת! ב-Python, כל ערך הוא אובייקט, עם נתונים ומתודות משלו:
name = "dana cohen"
print(name.title())
print(name.upper())
print(type(name))
numbers = [3, 1, 2]
numbers.append(5)
numbers.sort()
print(numbers)
print(type(numbers))פלט:
Dana Cohen
DANA COHEN
<class 'str'>
[1, 2, 3, 5]
<class 'list'>פירוק הקוד שורה אחר שורה
| קוד | מה זה ב-OOP |
|---|---|
name = "dana cohen" | נוצר אובייקט מסוג str. הנתונים שלו: התווים. |
name.title() | קריאה למתודה title של האובייקט. היא “יודעת” לעבוד על הטקסט שלו. |
type(name) → <class 'str'> | ה-Class (התבנית) של האובייקט היא str. |
numbers.append(5) | מתודה של אובייקט list שמשנה את הנתונים שלו. |
<class 'list'> | גם list היא Class. כל רשימה שיצרתם היא Object של ה-Class list. |
נקודת מפתח: str ו-list הן Classes ש-Python כתבה בשבילנו. השבוע נלמד לכתוב Classes משלנו: BankAccount, Student, Product, שמתאימים לבעיה שלנו.
דוגמה נוספת
עוד בעיה פרוצדורלית: מערכת סטודנטים. קחו את הקוד הבא, ונסו לזהות לבד את הבעיות.
student1 = {"name": "Dana", "grades": [90, 85]}
student2 = {"name": "Avi", "grades": [70]}
student3 = {"nmae": "Noa", "grades": []}
def add_grade(student, grade):
student["grades"].append(grade)
def average(student):
return sum(student["grades"]) / len(student["grades"])
add_grade(student1, 100)
student2["grades"].append(500)
print(average(student1))
print(average(student2))
print(average(student3))91.66666666666667
285.0
Traceback (most recent call last):
...
ZeroDivisionError: division by zeroהבעיות:
student3נוצר עם שגיאת כתיב ("nmae"). אין שום מקום שמגדיר מה “חייב” להיות בסטודנט.student2["grades"].append(500)עקף אתadd_grade. גם אילוadd_gradeהייתה בודקת שהציון בין 0 ל-100, הבדיקה לא הייתה רצה. ממוצע 285!averageקורסת על סטודנט בלי ציונים. ומי שמשתמש ב-dict של סטודנט צריך לדעת שהפונקציה הזו קיימת, ולזכור לטפל במקרה הזה.
ב-OOP, Student תהיה Class אחת עם מבנה מוגדר (שם וציונים, תמיד), עם מתודה add_grade שהיא הדרך היחידה להוסיף ציון (ובודקת אותו), ועם מתודה average שיודעת לטפל ברשימה ריקה. הכללים יושבים ליד הנתונים שהם שומרים עליהם.
סיכום
- תכנות פרוצדורלי: נתונים (dicts) ופונקציות בנפרד. עובד, אבל כשהמערכת גדלה, הנתונים חשופים, הפעולות מפוזרות, ואין מבנה מחייב.
- OOP: מארגנים את הקוד סביב דברים. כל אובייקט מחזיק נתונים (Attributes) ופעולות (Methods) יחד.
- Class = תבנית (תוכנית בנייה, תבנית ייצור). Object = דבר ספציפי שנוצר ממנה.
- ב-Python כל ערך הוא אובייקט:
strו-listהן Classes, ו-"hi".upper()היא קריאה למתודה. - OOP הוא דרך חשיבה: אילו דברים יש, מה הם יודעים, ומה הם עושים. לא כל דבר צריך להיות Class.
דוקומנטציה רשמית
- Python Tutorial — Classes — הפרק הרשמי על מחלקות
פרק שני: Class ו-Object — מגדירים תבנית ויוצרים ממנה אובייקטים
מה נלמד?
- מהי Class ומהו Object (נקרא גם Instance).
- מה המשמעות של המילה
class. - התחביר להגדרת Class.
- איך יוצרים Object מתוך Class.
- למה כל Object עצמאי.
למה צריך את זה?
בפרק הקודם ראינו שהבעיה של “dict + פונקציות” היא שאין מבנה מוגדר ואין מקום אחד שמרכז את הנתונים והפעולות. Class היא הכלי שיוצר את המבנה הזה. כל השאר (__init__, self, מתודות) בנוי עליה.
הסבר מהבסיס
מהי Class?
Class (מחלקה) היא תבנית (Blueprint) שמגדירה סוג חדש של דברים: איזה מידע יהיה לכל דבר מהסוג הזה, ומה הוא יוכל לעשות.
- Class לא מכילה נתונים של דבר ספציפי. היא רק ההגדרה.
- מ-Class אחת אפשר ליצור כמה דברים שרוצים.
- כשמגדירים Class, יוצרים בעצם טיפוס חדש ב-Python, בדיוק כמו
strאוlist.
מהו Object (Instance)?
Object (אובייקט / עצם) הוא דבר ספציפי שנוצר מתוך Class. יש לו נתונים משלו.
Instance (מופע) היא מילה נרדפת: “Object של ה-Class Student” = “Instance של Student”. את הפעולה של יצירת Object מ-Class קוראים Instantiation.
| Class | Object / Instance | |
|---|---|---|
| מה זה | תבנית, הגדרה | דבר ספציפי |
| כמה יש | אחת לכל סוג | כמה שרוצים |
| נתונים | מגדירה אילו שדות יהיו | מחזיק ערכים ספציפיים |
| דוגמה | Student | דנה, אבי, נועה |
| אנלוגיה | תבנית ייצור מכוניות | המכונית האדומה עם מספר 12-345-67 |
המילה class
class היא מילה שמורה (Keyword) ב-Python, כמו def, if ו-for. היא אומרת ל-Python: “מכאן מתחילה הגדרה של סוג חדש”. אחריה בא שם ה-Class, ואחריו גוף מוזח.
שמות של Classes
לפי PEP 8, שמות Classes נכתבים ב-PascalCase: כל מילה מתחילה באות גדולה, בלי קווים תחתונים. Student, BankAccount, ShoppingCart. זה שונה מפונקציות ומשתנים (snake_case), וכך רואים מיד אם משהו הוא Class.
תחביר
class ClassName:
# class body - indented
pass
my_object = ClassName()| חלק | הסבר |
|---|---|
class | מילה שמורה: “מתחילה הגדרה של Class”. |
ClassName | שם ה-Class, ב-PascalCase. |
: | חובה, כמו אחרי def או if. |
| גוף מוזח | מה שהתבנית כוללת (בפרקים הבאים: __init__ ומתודות). |
pass | “אל תעשה כלום”. גוף של Class (או פונקציה) לא יכול להיות ריק, ולכן pass הוא ממלא-מקום. |
ClassName() | יצירת Object: קוראים ל-Class עם סוגריים, כמו לפונקציה. מתקבל Object חדש. |
דוגמת קוד ראשונה
week-3/first_class.py:
class Person:
pass
dana = Person()
avi = Person()
print(dana)
print(avi)
print(type(dana))
print(dana is avi)
print(isinstance(dana, Person))פלט:
<__main__.Person object at 0x000001F4A3B2C1D0>
<__main__.Person object at 0x000001F4A3B2C2E0>
<class '__main__.Person'>
False
Trueפירוק הקוד שורה אחר שורה
| שורה | הסבר |
|---|---|
class Person: | מגדירה סוג חדש בשם Person. עדיין לא נוצר שום אדם. |
pass | התבנית ריקה בינתיים. |
dana = Person() | Instantiation: נוצר Object חדש מסוג Person, והשם dana מצביע עליו. |
avi = Person() | Object נוסף, נפרד לגמרי. |
print(dana) | אין ל-Object “תוכן” להציג, ולכן Python מדפיסה תיאור ברירת מחדל: הסוג, וכתובת בזיכרון (0x...). הכתובת אצלכם תהיה שונה. |
| שתי כתובות שונות | שני אובייקטים נפרדים ב-RAM. |
type(dana) | <class '__main__.Person'>. ה-Class של האובייקט היא Person, שהוגדרה בקובץ הראשי (__main__). |
dana is avi | False: לא אותו אובייקט (נושא Python, is!). |
isinstance(dana, Person) | True: “האם dana היא Instance של Person?” |
דוגמה נוספת
להוסיף נתונים לאובייקט — ולראות למה זו לא הדרך. אפשר להצמיד לאובייקט Attribute (תכונה) מבחוץ, עם נקודה:
class Product:
pass
laptop = Product()
laptop.name = "Laptop"
laptop.price = 3500
mouse = Product()
mouse.name = "Mouse"
mouse.price = 80
print(laptop.name, laptop.price)
print(mouse.name, mouse.price)
mouse.price = 70
print("After update:", mouse.price, "| laptop unchanged:", laptop.price)פלט:
Laptop 3500
Mouse 80
After update: 70 | laptop unchanged: 3500פירוק:
laptop.name = "Laptop"— יוצר Attribute בשםnameעל האובייקטlaptop. הנקודה פירושה “של”:laptop.name= “השם של ה-laptop”.- לכל אובייקט Attributes משלו. שינוי
mouse.priceלא נוגע ב-laptop. - זה כבר יותר טוב מ-dict:
laptop.priceקריא יותר מ-laptop["price"].
אבל יש כאן בעיה גדולה. מה מבטיח שלכל Product יש name ו-price?
keyboard = Product()
keyboard.nmae = "Keyboard"
print(keyboard.name)AttributeError: 'Product' object has no attribute 'name'- שגיאת כתיב (
nmae) יצרה Attribute אחר. - שכחנו להגדיר
price, ואין שום אזהרה עד שמישהו ינסה להשתמש בו. - זו בדיוק הבעיה של ה-dict מהפרק הקודם! ה-Class הריקה עדיין לא מגדירה מבנה.
הפתרון: שה-Class עצמה תגדיר אילו נתונים חייבים להיות לכל אובייקט, ותמלא אותם ברגע היצירה. זה התפקיד של __init__, נושא הפרק הבא.
סיכום
- Class = תבנית שמגדירה סוג חדש. Object / Instance = דבר ספציפי שנוצר ממנה.
- תחביר:
class Name:+ גוף מוזח (אוpass). שמות ב-PascalCase. - יצירת Object:
obj = Name(), עם סוגריים. בלי סוגריים, זו ה-Class עצמה. - כל Object עצמאי:
a is b→False. Attributes של אחד לא משפיעים על השני. type(obj)מחזירה את ה-Class.isinstance(obj, Name)בודקת שייכות.- הוספת Attributes “מבחוץ” עובדת, אבל לא מגדירה מבנה. לכן צריך
__init__.
דוקומנטציה רשמית
- A First Look at Classes — class ו-Object
- Class Objects — יצירת אובייקט מהמחלקה
פרק שלישי: __init__ ו-self — אובייקט שנולד שלם
מה נלמד?
- מהו
__init__, הפונקציה שרצה אוטומטית כשיוצרים אובייקט. - מהו Constructor.
- מהו
self, המילה שמופיעה בכל מקום ב-OOP. - Instance Attributes: הנתונים של כל אובייקט.
למה צריך את זה?
בפרק הקודם הוספנו Attributes “מבחוץ”: laptop.name = "Laptop". ראינו שזה מסוכן: אפשר לשכוח שדה, לטעות בכתיב, ואין מבנה מחייב. כתבנו אפילו פונקציית עזר make_book שתבטיח שלכל ספר יהיו title ו-pages.
__init__ הוא בדיוק ה-make_book הזה, רק בתוך ה-Class. הוא מבטיח שכל אובייקט ייווצר מאותחל: עם כל השדות שלו, ועם ערכים, כבר מהרגע הראשון.
הסבר מהבסיס
__init__ — מתודת האתחול
__init__ (קוראים: “דאנדר אינִיט”, double underscore init) היא פונקציה מיוחדת שמגדירים בתוך Class. היא:
- נקראת אוטומטית ברגע שיוצרים אובייקט חדש:
Student(...). - מאתחלת (Initialize) את האובייקט, כלומר שמה בו את הנתונים ההתחלתיים.
- לא מחזירה כלום (
returnלא נדרש).
השם עם שני קווים תחתונים מכל צד מסמן מתודה מיוחדת של Python (נקראת Dunder Method או Magic Method). Python מחפשת שם בדיוק כזה, וקוראת לו בעצמה. אתם אף פעם לא כותבים obj.__init__() בעצמכם.
Constructor
Constructor (בנאי) = הקוד שרץ כשבונים אובייקט חדש. ב-Python, __init__ משמש בתפקיד הזה, ולכן קוראים לו לרוב “ה-Constructor”. (בדיוק טכני: הבנייה עצמה, הקצאת האובייקט בזיכרון, נעשית בשלב לפני, ו-__init__ “רק” ממלא אותו. לצורכי הקורס, __init__ = Constructor.)
self — “האובייקט הזה”
self הוא הפרמטר הראשון של כל מתודה ב-Class, כולל __init__. הוא מצביע על האובייקט הספציפי שעליו המתודה עובדת כרגע.
למה צריך אותו? ה-Class היא תבנית אחת, אבל ממנה נוצרים הרבה אובייקטים. כשהקוד ב-__init__ כותב “שמור את השם”, הוא צריך לדעת באיזה אובייקט לשמור. self הוא התשובה: “באובייקט שנוצר עכשיו”.
self.name = nameנקרא: “באובייקט הזה (self), צור Attribute בשם name, ושים בו את הערך שהתקבל בפרמטר name”.
self.name— ה-Attribute של האובייקט. נשאר כל עוד האובייקט קיים.name(לבד) — הפרמטר של__init__: משתנה מקומי (Local, נושא Python!) שנעלם בסוף__init__.
מאיפה self מגיע? Python מעבירה אותו אוטומטית! כשכותבים Student("Dana", 24), אתם מעבירים 2 ארגומנטים, אבל __init__ מקבל 3: Python מוסיפה את האובייקט החדש כ-self, בהתחלה.
ה-Class היא טופס ריק עם שדות: “שם: ____ גיל: ____”. __init__ הוא פקיד הרישום שממלא טופס חדש לכל סטודנט שמגיע. self הוא “הטופס שעל השולחן עכשיו”: הפקיד כותב “שם: דנה” על הטופס הזה, ולא על כל הטפסים בארון. מחר יגיע אבי, ויתמלא טופס אחר, אבל הפקיד יקרא לו שוב “הטופס שעל השולחן” (self).
מה קורה בשורה dana = Student("Dana", 24)
- Python יוצרת אובייקט ריק חדש מסוג
Studentבזיכרון. - Python קוראת ל-
__init__ומעבירה לו:self= האובייקט החדש,name = "Dana",age = 24. __init__מריץself.name = name, כלומרnameשל האובייקט החדש ="Dana". וכך גםage.__init__מסתיים. הפרמטרים המקומיים נעלמים, וה-Attributes נשארים על האובייקט.- Python מחזירה את האובייקט, והשם
danaמצביע עליו.
Instance Attributes
Instance Attributes = נתונים ששייכים לאובייקט ספציפי. לכל אובייקט ערכים משלו. מגדירים אותם ב-__init__ עם self.something = ....
תחביר
class ClassName:
def __init__(self, param1, param2):
self.attribute1 = param1
self.attribute2 = param2
self.attribute3 = "default value" # attributes do not have to come from parameters
obj = ClassName(arg1, arg2) # Python calls __init__(obj, arg1, arg2) for you
obj.attribute1 # read an instance attribute__init__נכתב בתוך ה-Class, מוזח.selfתמיד ראשון ברשימת הפרמטרים.- בקריאה לא מעבירים
self. - אפשר לתת לפרמטרים ערכי ברירת מחדל, כמו בכל פונקציה:
def __init__(self, name, age=18):.
דוגמת קוד ראשונה
week-3/student.py:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
self.grades = []
dana = Student("Dana", 24)
avi = Student("Avi", 31)
print(dana.name, dana.age, dana.grades)
print(avi.name, avi.age, avi.grades)
dana.grades.append(95)
print("Dana's grades:", dana.grades)
print("Avi's grades:", avi.grades)פלט:
Dana 24 []
Avi 31 []
Dana's grades: [95]
Avi's grades: []פירוק הקוד שורה אחר שורה
| שורה | הסבר |
|---|---|
class Student: | מגדירים את התבנית. |
def __init__(self, name, age): | ה-Constructor. self = האובייקט הנוצר. name, age = מה שמעבירים ביצירה. |
self.name = name | Attribute name על האובייקט, עם הערך מהפרמטר. |
self.age = age | אותו דבר לגיל. |
self.grades = [] | Attribute שלא מגיע מפרמטר: כל סטודנט מתחיל עם רשימת ציונים ריקה משלו. |
dana = Student("Dana", 24) | Python: יוצרת אובייקט, קוראת __init__(new_obj, "Dana", 24), ומחזירה אותו. |
avi = Student("Avi", 31) | אובייקט שני. __init__ רץ שוב, עם self = האובייקט השני. |
dana.grades.append(95) | ניגשים לרשימה של דנה ומוסיפים. |
avi.grades נשאר [] | כל __init__ יצר רשימה חדשה, ולכן אין שיתוף. (השוו לבאג של def f(cart=[]) בנושא Python!) |
עכשיו המבנה מובטח: אי אפשר ליצור Student בלי שם וגיל, ולכל סטודנט תמיד יש grades.
דוגמה נוספת
Product עם ערכי ברירת מחדל, Attribute מחושב, ולידציה ראשונה. week-3/product.py:
class Product:
def __init__(self, name, price, quantity=0):
if price < 0:
raise ValueError("price cannot be negative, got " + str(price))
self.name = name
self.price = price
self.quantity = quantity
self.in_stock = quantity > 0
laptop = Product("Laptop", 3500, 4)
cable = Product("USB Cable", 25)
print(laptop.name, laptop.price, laptop.quantity, laptop.in_stock)
print(cable.name, cable.price, cable.quantity, cable.in_stock)
try:
broken = Product("Ghost", -10)
except ValueError as error:
print("Could not create product:", error)פלט:
Laptop 3500 4 True
USB Cable 25 0 False
Could not create product: price cannot be negative, got -10פירוק:
quantity=0— ערך ברירת מחדל.Product("USB Cable", 25)מקבל כמות 0.self.in_stock = quantity > 0— Attribute מחושב מתוך הנתונים:TrueאוFalse.- ולידציה ב-Constructor:
__init__מסרב ליצור מוצר עם מחיר שלילי, וזורקValueError(נושא Python!). אי אפשר שיתקיים מוצר לא חוקי במערכת. זה בדיוק “שמירת הכללים ליד הנתונים” שדיברנו עליה בפרק הראשון. - כש-
__init__זורק שגיאה, האובייקט לא נוצר, ו-brokenלא מקבל ערך.
Person — דוגמה שלישית, לחיזוק
class Person:
def __init__(self, first_name, last_name, city="Unknown"):
self.first_name = first_name
self.last_name = last_name
self.city = city
self.full_name = first_name + " " + last_name
noa = Person("Noa", "Levi", "Haifa")
tom = Person("Tom", "Cohen")
print(noa.full_name, "-", noa.city)
print(tom.full_name, "-", tom.city)Noa Levi - Haifa
Tom Cohen - Unknownסיכום
__init__= ה-Constructor: נקרא אוטומטית ביצירת אובייקט, ומאתחל את הנתונים שלו.self= האובייקט הספציפי שהמתודה עובדת עליו כרגע. פרמטר ראשון תמיד, ו-Python מעבירה אותו לבד.self.x = x:self.x= Attribute שנשמר על האובייקט.x= פרמטר מקומי שנעלם.obj = Class(a, b)→ Python יוצרת אובייקט, קוראת__init__(obj, a, b), ומחזירה את האובייקט.- ב-
__init__אפשר: ערכי ברירת מחדל, Attributes מחושבים, וולידציה עםraise, כך שאובייקט לא חוקי לא נוצר בכלל.
דוקומנטציה רשמית
- Class Objects — init — init ו-self
- object.init — ההגדרה של init
פרק רביעי: Methods — הפעולות של האובייקט
מה נלמד?
- מהי Method (מתודה).
- איך מגדירים אותה ואיך קוראים לה.
- שלושה סוגי מתודות לפי מה שהן עושות: מחשבות, משנות מצב, מדפיסות.
- מתודה שקוראת למתודה אחרת.
__str__: איך אובייקט “מציג את עצמו”.
למה צריך את זה?
עד עכשיו לאובייקטים שלנו היו רק נתונים. אבל הרעיון של OOP הוא נתונים + פעולות ביחד. בפרק הראשון ראינו שהכללים (כמו “אי אפשר למשוך יותר מהיתרה”) צריכים לשבת ליד הנתונים. מתודות הן המקום שבו הכללים האלה גרים.
וגם בעיה פתוחה מהפרק הקודם: description של המכונית חושב פעם אחת ב-__init__, והתיישן כשהצבע השתנה. מתודה מחשבת בכל פעם מחדש.
הסבר מהבסיס
מהי Method?
Method = פונקציה שמוגדרת בתוך Class, ופועלת על אובייקט ספציפי מאותה Class.
| Function (פונקציה רגילה) | Method (מתודה) | |
|---|---|---|
| איפה מוגדרת | ברמה הראשית של הקובץ | בתוך Class |
| איך קוראים לה | function_name(args) | obj.method_name(args) |
| על מה היא עובדת | על מה שמעבירים לה | על האובייקט שלה (self), ועל מה שמעבירים לה |
| פרמטר ראשון | כל מה שרוצים | self |
כבר השתמשתם במתודות: "hi".upper(), numbers.append(5). היום כותבים משלנו.
self במתודות
בדיוק כמו ב-__init__: כשקוראים dana.add_grade(90), Python מעבירה אוטומטית את dana כ-self. בתוך המתודה, self.grades = הציונים של דנה. אם קוראים avi.add_grade(80), אותה מתודה בדיוק רצה, אבל self הוא אבי.
dana.add_grade(90) → Student.add_grade(dana, 90)
↑ ↑
self gradeהשורה השמאלית היא מה שאנחנו כותבים. השורה הימנית היא מה ש-Python עושה בפועל.
שלושה סוגי מתודות (לפי מה שהן עושות)
| סוג | מה עושה | דוגמה |
|---|---|---|
| Getter | מחשבת ומחזירה ערך מהנתונים, בלי לשנות כלום | average(), is_passing() |
| Setter | משנה את המצב (State) של האובייקט | add_grade(90), deposit(100) |
| הצגה | מדפיסה / מחזירה טקסט לתצוגה | print_report(), __str__() |
הבחנה שימושית מ-Clean Code: מתודה טובה היא בדרך כלל או Getter או Setter, לא שתיהן יחד (SRP!).
מצב (State)
State = הערכים של כל ה-Attributes של אובייקט ברגע מסוים. חשבון עם יתרה 1,000 נמצא במצב מסוים. אחרי deposit(200), הוא במצב חדש. מתודות Setter הן הדרך המסודרת לשנות מצב.
__str__ — איך האובייקט נראה ב-print
זוכרים ש-print(dana) הדפיס <__main__.Student object at 0x...>? אם מגדירים ב-Class מתודה מיוחדת בשם __str__ (שוב Dunder!) שמחזירה מחרוזת, print ו-str() ישתמשו בה כדי להציג את האובייקט.
תחביר
class ClassName:
def __init__(self, ...):
self.data = ...
def getter_method(self): # returns something, changes nothing
return ... self.data ...
def setter_method(self, value): # changes the object's state
self.data = ...
def uses_other_method(self):
return self.getter_method() * 2 # call another method on the same object
def __str__(self): # used by print() and str()
return "readable text about " + str(self.data)
obj = ClassName(...)
obj.setter_method(5) # no self in the call
result = obj.getter_method()
print(obj) # calls __str__ automatically- כל מתודה מוזחת בתוך ה-Class, ו-
selfראשון. - בין מתודות שמים שורה ריקה אחת (PEP 8).
- בתוך מתודה, ניגשים לנתונים ולמתודות אחרות עם
self..
דוגמת קוד ראשונה
week-3/student_methods.py:
PASSING_GRADE = 60
class Student:
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
if grade < 0 or grade > 100:
raise ValueError("grade must be between 0 and 100, got " + str(grade))
self.grades.append(grade)
def average(self):
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)
def is_passing(self):
return self.average() >= PASSING_GRADE
def __str__(self):
return self.name + " (average " + str(round(self.average(), 1)) + ")"
dana = Student("Dana")
dana.add_grade(90)
dana.add_grade(75)
avi = Student("Avi")
avi.add_grade(50)
print(dana)
print(avi)
print("Dana passing?", dana.is_passing())
print("Avi passing?", avi.is_passing())פלט:
Dana (average 82.5)
Avi (average 50.0)
Dana passing? True
Avi passing? Falseפירוק הקוד שורה אחר שורה
| קוד | סוג | הסבר |
|---|---|---|
def add_grade(self, grade): | Setter | משנה את המצב: מוסיפה ל-self.grades. וקודם בודקת את הכלל (0–100). |
def average(self): | Getter | מחשבת מהנתונים של האובייקט ומחזירה. לא משנה כלום. מטפלת ברשימה ריקה (הבאג מהפרק הראשון!). |
def is_passing(self): | Getter | קוראת למתודה אחרת של אותו אובייקט: self.average(). |
def __str__(self): | הצגה | מחזירה מחרוזת. print(dana) קורא לה אוטומטית. |
dana.add_grade(90) | Python: Student.add_grade(dana, 90). self = dana. | |
print(avi) | Python: print(avi.__str__()), כלומר "Avi (average 50.0)". |
הנקודה החשובה: הקוד שמשתמש ב-Class (dana.add_grade(...)) לא צריך לדעת איך הציונים נשמרים (רשימה), ולא צריך לבדוק טווח בעצמו. ה-Class אחראית לזה.
דוגמה נוספת
חשבון בנק — הבעיה מהפרק הראשון, פתורה. week-3/bank_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
self.transactions = []
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive, got " + str(amount))
self.balance = self.balance + amount
self.transactions.append("deposit " + str(amount))
def withdraw(self, amount):
if amount <= 0:
raise ValueError("withdrawal must be positive, got " + str(amount))
if amount > self.balance:
raise ValueError("insufficient funds: balance " + str(self.balance) + ", requested " + str(amount))
self.balance = self.balance - amount
self.transactions.append("withdraw " + str(amount))
def transfer_to(self, other_account, amount):
self.withdraw(amount)
other_account.deposit(amount)
def statement(self):
print("--- Statement for", self.owner, "---")
for transaction in self.transactions:
print(" ", transaction)
print(" Balance:", self.balance)
def __str__(self):
return "Account of " + self.owner + ": " + str(self.balance) + " NIS"
dana = BankAccount("Dana", 1000)
avi = BankAccount("Avi")
dana.deposit(200)
dana.transfer_to(avi, 300)
try:
avi.withdraw(5000)
except ValueError as error:
print("Rejected:", error)
print(dana)
print(avi)
dana.statement()פלט:
Rejected: insufficient funds: balance 300, requested 5000
Account of Dana: 900 NIS
Account of Avi: 300 NIS
--- Statement for Dana ---
deposit 200
withdraw 300
Balance: 900פירוק:
transfer_to(self, other_account, amount)— מתודה שמקבלת אובייקט אחר כפרמטר. היא לא משנה את היתרות ישירות. היא משתמשת ב-self.withdrawוב-other_account.deposit, וכך כל הבדיקות רצות אוטומטית. DRY!- אם
withdrawנכשלת (אין מספיק כסף), היא זורקת שגיאה לפני ה-deposit, ולכן אין “כסף שנוצר מאוויר”. self.transactions— היסטוריה פנימית שכל פקודה מעדכנת. מי שמשתמש בחשבון לא צריך לזכור לרשום תנועה.statement()היא מתודת הצגה.__str__נותן תיאור קצר ל-print.- השוו לגרסה הפרוצדורלית מהפרק הראשון: אותן פעולות, אבל עכשיו יש מקום אחד שבו מוגדר מה זה חשבון ומה מותר לעשות איתו.
סיכום
- Method = פונקציה בתוך Class. מגדירים עם
selfראשון, וקוראיםobj.method(args)בליself. obj.method(x)שקול ל-Class.method(obj, x). כךself“יודע” על איזה אובייקט לעבוד.- בתוך מתודה,
self.attributeו-self.other_method(). - Getter מחזירה ולא משנה. Setter משנה את ה-State (ובודקת את הכללים).
__str__מחזירה מחרוזת, ו-print(obj)משתמש בה.- מידע נגזר עדיף לחשב במתודה, ולא לשמור כ-Attribute שעלול להתיישן.
דוקומנטציה רשמית
- Method Objects — איך Method מקבלת את self
פרק חמישי: נתונים משותפים — Instance Attribute מול Class Attribute
מה נלמד?
- Attribute מול Method: איך מחליטים מה כל דבר.
- Instance Attribute: נתון של כל אובייקט בנפרד (מה שעשינו עד עכשיו).
- Class Attribute: נתון של ה-Class, משותף לכל האובייקטים.
- מתי משתמשים בכל אחד, ואילו מלכודות יש.
למה צריך את זה?
נניח שיש Class Product, וכל המוצרים כפופים לאותו מע"מ: 17%. איפה שומרים את המע"מ?
- כ-Instance Attribute (
self.vat = 0.17)? אז יש אלף עותקים של אותו מספר, ואם המע"מ משתנה, צריך לעדכן אלף אובייקטים. - כקבוע גלובלי בראש הקובץ? אפשרי, אבל הוא “רחוק” מה-Class שהוא שייך לה.
ומה אם רוצים לדעת כמה מוצרים נוצרו בסך הכול? אף מוצר בודד לא “יודע” את זה. זה מידע על הסוג כולו. Class Attributes פותרים את שני המקרים.
הסבר מהבסיס
Attribute או Method?
| Attribute | Method | |
|---|---|---|
| מה זה | נתון, מה האובייקט יודע / מחזיק | פעולה, מה האובייקט עושה / מחשב |
| חלק דיבר | שם עצם / תואר | פועל |
| דוגמאות | name, price, balance, is_active | deposit(), add_grade(), total() |
| גישה | obj.price | obj.total() עם סוגריים |
שאלות שעוזרות להחליט:
- האם זה נתון שנשמר, או משהו שמחושב? נתון שנשמר ← Attribute. מחושב מנתונים אחרים ← בדרך כלל Method (ראינו:
descriptionשהתיישן). - האם זה משנה את האובייקט? כל שינוי מסודר ← Method (פקודה).
- האם צריך כללים כשמשנים אותו? אם כן, השינוי עובר דרך Method (ונעמיק בזה בשיעור על Encapsulation).
Instance Attribute
מוגדר עם self.x = ..., בדרך כלל ב-__init__. לכל אובייקט עותק משלו, עם ערך משלו.
Class Attribute
מוגדר ישירות בגוף ה-Class, מחוץ לכל מתודה, בלי self:
class Product:
VAT_RATE = 0.17 # class attribute - one shared value
count = 0 # class attribute
def __init__(self, name):
self.name = name # instance attribute - per object- יש עותק אחד בלבד, ששייך ל-Class עצמה.
- כל האובייקטים רואים אותו.
- ניגשים אליו דרך ה-Class (
Product.VAT_RATE, הדרך המומלצת), או דרך אובייקט (laptop.VAT_RATE, עובד לקריאה).
Instance Attributes = מה שכל תלמיד מחזיק לעצמו: שם, ציונים, מחברת. Class Attributes = מה שתלוי על הלוח של הכיתה ומשותף לכולם: שם המחנך, מספר החדר, וכמה תלמידים יש בכיתה. כשהמחנך מתחלף, מעדכנים את הלוח פעם אחת, ולא במחברת של כל תלמיד.
איך Python מוצאת Attribute
כשכותבים obj.x, Python מחפשת:
- קודם על האובייקט (Instance Attributes).
- אם לא נמצא, על ה-Class (Class Attributes ומתודות).
- לא נמצא ←
AttributeError.
(שימו לב: זה דומה ל-LEGB מנושא Python, חיפוש בשכבות.) מכאן גם המלכודת שנראה בטעות הנפוצה.
תחביר
class ClassName:
SHARED_CONSTANT = 100 # class attribute (constant style: UPPER_CASE)
counter = 0 # class attribute (changes, shared)
def __init__(self, value):
self.value = value # instance attribute
ClassName.counter = ClassName.counter + 1 # update the class attribute through the CLASS
def uses_constant(self):
return self.value * ClassName.SHARED_CONSTANT
ClassName.SHARED_CONSTANT # read through the class
ClassName.counterדוגמת קוד ראשונה
week-3/products_shared.py:
class Product:
VAT_RATE = 0.17
created_count = 0
def __init__(self, name, price):
self.name = name
self.price = price
Product.created_count = Product.created_count + 1
def price_with_vat(self):
return round(self.price * (1 + Product.VAT_RATE), 2)
laptop = Product("Laptop", 1000)
mouse = Product("Mouse", 50)
print(laptop.name, laptop.price_with_vat())
print(mouse.name, mouse.price_with_vat())
print("Products created:", Product.created_count)
Product.VAT_RATE = 0.18
print("After VAT change:", laptop.price_with_vat(), mouse.price_with_vat())פלט:
Laptop 1170.0
Mouse 58.5
Products created: 2
After VAT change: 1180.0 59.0פירוק הקוד שורה אחר שורה
| קוד | סוג | הסבר |
|---|---|---|
VAT_RATE = 0.17 | Class Attribute | ערך אחד לכל המוצרים. אותיות גדולות = “קבוע” (מוסכמה). |
created_count = 0 | Class Attribute | מונה משותף. מתחיל ב-0 כשה-Class מוגדרת. |
self.name, self.price | Instance Attributes | שונים בכל מוצר. |
Product.created_count = Product.created_count + 1 | כל __init__ מעדכן את המונה של ה-Class, ולכן הוא סופר את כל האובייקטים. | |
Product.VAT_RATE במתודה | קריאה ל-Class Attribute. | |
Product.VAT_RATE = 0.18 | שינוי אחד, וכל המוצרים מושפעים מיד, כי כולם קוראים מאותו מקום. |
דוגמה נוספת
כיתה עם מגבלת מקומות, ומזהה ייחודי אוטומטי. week-3/students_ids.py:
class Student:
SCHOOL_NAME = "Kodkod 6"
MAX_STUDENTS = 3
next_id = 1
all_students = []
def __init__(self, name):
if len(Student.all_students) >= Student.MAX_STUDENTS:
raise ValueError("class is full - cannot add " + name)
self.name = name
self.student_id = Student.next_id
Student.next_id = Student.next_id + 1
Student.all_students.append(self)
def __str__(self):
return "#" + str(self.student_id) + " " + self.name + " (" + Student.SCHOOL_NAME + ")"
for name in ["Dana", "Avi", "Noa", "Tom"]:
try:
Student(name)
except ValueError as error:
print("Error:", error)
for student in Student.all_students:
print(student)
print("Next id will be:", Student.next_id)פלט:
Error: class is full - cannot add Tom
#1 Dana (Kodkod 6)
#2 Avi (Kodkod 6)
#3 Noa (Kodkod 6)
Next id will be: 4פירוק:
next_id— מונה שמייצר מזהה ייחודי לכל סטודנט. כל אובייקט שומר את המספר שלו ב-Instance Attributestudent_id, והמונה המשותף מתקדם.all_students— רשימה משותפת שבה ה-Class “רושמת” כל אובייקט שנוצר.append(self)= “תוסיף את האובייקט הזה לרשימה”.MAX_STUDENTS— כלל שחל על כל ה-Class. הבדיקה רצה לפני שה-ID מוקצה, ולכן Tom לא “שרף” מספר.Student(name)בלולאה בלי לשמור במשתנה: האובייקט לא הולך לאיבוד, כי הוא נרשם ב-all_students.
Class Attributes מצוינים לקבועים (VAT_RATE, MAX_STUDENTS) ולמונים. רשימה משותפת של “כל האובייקטים” היא כלי להמחשה טוב, אבל בפרויקטים אמיתיים עדיף בדרך כלל לנהל את האוסף ב-Class נפרדת (למשל School שמחזיקה רשימת סטודנטים). נלמד את זה בנושא תכנון OOP (Composition).
סיכום
- Attribute = מה האובייקט יודע (שם עצם). Method = מה הוא עושה (פועל, עם סוגריים). מידע מחושב ← Method.
- Instance Attribute (
self.xב-__init__) = לכל אובייקט ערך משלו. - Class Attribute (בגוף ה-Class, בלי
self) = עותק אחד משותף. מתאים לקבועים ולמונים. - Python מחפשת
obj.xקודם באובייקט, ואז ב-Class. - שינוי Class Attribute רק דרך שם ה-Class.
obj.X = ...יוצר Instance Attribute חדש. - רשימה/dict “לכל אובייקט” ← ב-
__init__, לא בגוף ה-Class.
דוקומנטציה רשמית
- Class and Instance Variables — Instance Attribute מול Class Attribute
פרק שישי: מה הופך ל-Class — מדרישה מילולית למחלקות
מה נלמד?
עד עכשיו אמרנו לכם אילו Classes לכתוב. היום עוברים לצד השני: אתם מחליטים.
- איך מוצאים Classes בתוך תיאור מילולי של מערכת.
- ארבע שאלות שמחליטות: Class, או לא?
- מתי משהו הוא Attribute, ומתי הוא Class משלו.
- לאיזו Class שייכת כל פעולה.
למה צריך את זה?
הסילבוס מדגיש: המטרה היא להבין OOP, ולא רק לדעת להעתיק Class. בעבודה, אף אחד לא יגיד לכם “כתוב Class בשם Order עם השדות האלה”. תקבלו דרישה: “צריך מערכת שבה לקוחות מזמינים מוצרים…”, ותצטרכו להפוך אותה למבנה של Classes בעצמכם.
תכנון טוב בהתחלה חוסך שבועות של תיקונים. תכנון גרוע מוביל ל-Class אחת ענקית, או לעשרות Classes ריקות שלא עושות כלום.
הסבר מהבסיס
כלי 1: ניתוח שמות עצם ופעלים
טכניקה פשוטה ויעילה: קוראים את הדרישה, ומסמנים:
| בטקסט | מועמד ל- |
|---|---|
| שמות עצם חשובים (“לקוח”, “הזמנה”, “מוצר”) | Classes או Attributes |
| תארים ומאפיינים (“מחיר”, “תאריך”, “פעיל”) | Attributes |
| פעלים (“מזמין”, “משלם”, “מבטל”) | Methods |
| כללים (“לא ניתן ל…”, “לכל היותר…”) | Validation / Invariants |
זו נקודת התחלה, לא תשובה. לא כל שם עצם הופך ל-Class.
כלי 2: ארבע שאלות — האם זו Class?
| # | שאלה | אם התשובה “כן” |
|---|---|---|
| 1 | יש לזה זהות ונתונים משלו? (לכל אחד ערכים שונים) | נטייה ל-Class |
| 2 | יש לזה התנהגות או כללים? (פעולות, ולידציה) | נטייה חזקה ל-Class |
| 3 | יש מזה כמה מופעים? (הרבה לקוחות, הרבה הזמנות) | נטייה ל-Class |
| 4 | זה רק ערך בודד, בלי שום התנהגות? (“צבע”, “שם”) | Attribute, לא Class |
“מכונית” — יש לה מספר רישוי משלה (זהות), היא נוסעת ובולמת (התנהגות), ויש הרבה מכוניות. ✓ Class. “צבע המכונית” — זה רק ערך (“אדום”), בלי פעולות משלו. Attribute של המכונית. “מנוע” — תלוי! במשחק פשוט, Attribute (engine_size = 1.6). במוסך שמתקן מנועים, Class (יש לו מספר סידורי, היסטוריית טיפולים ומצב). אותו מושג, החלטה אחרת לפי הצורך של המערכת.
כלי 3: Attribute פשוט או Class משלו?
שאלה מכרעת: האם צריך לשמור על הדבר הזה יותר ממידע אחד, או להפעיל עליו כללים?
| מושג | Attribute פשוט כש… | Class משלו כש… |
|---|---|---|
| כתובת | מספיק טקסט אחד: "Herzl 10, Haifa" | צריך עיר, רחוב ומיקוד בנפרד, ולסנן לפי עיר, ולאמת מיקוד |
| מחיר | מספר אחד | צריך מטבע, המרות ועיגול לפי מטבע |
| ציון | מספר | צריך גם תאריך, שם המבחן, משקל וערעור |
כלל אצבע: מתחילים פשוט (Attribute). כשהערך “גדל” ומקבל עוד שדות וכללים, הופכים אותו ל-Class.
כלי 4: לאיזו Class שייכת המתודה?
כלל הזהב: המתודה הולכת ל-Class שמחזיקה את הנתונים שהיא צריכה. (נקרא “Information Expert”.)
- “חשב את סכום ההזמנה” ← צריך את פריטי ההזמנה ←
Order.total(). - “האם המוצר במלאי?” ← צריך את הכמות של המוצר ←
Product.is_in_stock(). - “כמה הלקוח הוציא השנה?” ← צריך את ההזמנות של הלקוח ←
Customer(אם הוא מחזיק את ההזמנות שלו), או Class שמנהלת את כל ההזמנות.
סימן אזהרה: מתודה ב-Class A שרובה other.x, other.y, other.z ← כנראה היא שייכת ל-Class של other.
תחביר
אין תחביר חדש. התוצר של השלב הזה הוא תכנון, שאפשר לכתוב כ"שלד" קוד, עם pass וב-docstrings:
class Order:
"""A customer's order: items, status and total."""
def __init__(self, customer):
self.customer = customer # has-a: the order holds its customer
self.items = []
self.status = "new"
def add_item(self, product, quantity):
pass
def total(self):
passשלד כזה מאפשר לחשוב על המבנה לפני שכותבים לוגיקה.
דוגמת קוד ראשונה
דרישה: “מערכת לבית קפה. לקוחות מזמינים משקאות מהתפריט. לכל משקה יש שם, גודל (קטן/גדול) ומחיר. משקה גדול עולה 3 ש"ח יותר. הזמנה יכולה לכלול כמה משקאות, ויש לה סכום כולל. לקוח יכול לבטל הזמנה, כל עוד היא לא הוכנה.”
שלב א’ — סימון:
- שמות עצם: לקוח, משקה, תפריט, הזמנה, שם, גודל, מחיר, סכום.
- פעלים: מזמינים, כולל, לבטל, הוכנה.
- כללים: גדול = +3. ביטול רק לפני הכנה.
שלב ב’ — ארבע השאלות:
| מועמד | זהות/נתונים? | התנהגות? | כמה מופעים? | החלטה |
|---|---|---|---|---|
| משקה | ✓ שם, גודל, מחיר | ✓ חישוב מחיר לפי גודל | ✓ | Class Drink |
| הזמנה | ✓ פריטים, סטטוס | ✓ סכום, ביטול, הכנה | ✓ | Class Order |
| לקוח | ✓ שם | מעט | ✓ | כאן: Attribute (שם) של Order. אין עליו התנהגות בדרישה. |
| תפריט | רשימת משקאות | אולי חיפוש | אחד | כאן: dict פשוט. אם יתווספו כללים, יהפוך ל-Class. |
| גודל | ערך (“small”/“large”) | — | — | Attribute של Drink |
| מחיר | מספר | — | — | Attribute + מתודה מחושבת |
| סכום | נגזר | — | — | Method Order.total() |
שלב ג’ — מתודות (“מי מחזיק את הנתונים?”):
price()→ Drink, כי Drink יודע את המחיר הבסיסי ואת הגודל.total()→ Order, כי Order מחזיקה את המשקאות.cancel(),mark_prepared()→ Order, כי Order מחזיקה את הסטטוס.
שלב ד’ — קוד. week-3/coffee_shop.py:
LARGE_EXTRA = 3
class Drink:
def __init__(self, name, base_price, size="small"):
if size not in ("small", "large"):
raise ValueError("size must be small or large, got " + size)
self.name = name
self.base_price = base_price
self.size = size
def price(self):
if self.size == "large":
return self.base_price + LARGE_EXTRA
return self.base_price
class Order:
def __init__(self, customer_name):
self.customer_name = customer_name
self.drinks = []
self.status = "open"
def add(self, drink):
if self.status != "open":
raise ValueError("cannot add drinks to a " + self.status + " order")
self.drinks.append(drink)
def total(self):
return sum(drink.price() for drink in self.drinks)
def mark_prepared(self):
self.status = "prepared"
def cancel(self):
if self.status == "prepared":
raise ValueError("cannot cancel - the order was already prepared")
self.status = "cancelled"
MENU = {"espresso": 9, "latte": 14, "tea": 8}
order = Order("Dana")
order.add(Drink("latte", MENU["latte"], "large"))
order.add(Drink("espresso", MENU["espresso"]))
print(order.customer_name, "pays", order.total())
order.mark_prepared()
try:
order.cancel()
except ValueError as error:
print("Rejected:", error)פלט:
Dana pays 26
Rejected: cannot cancel - the order was already preparedפירוק הקוד שורה אחר שורה
| החלטת תכנון | איך היא נראית בקוד |
|---|---|
| משקה = Class | class Drink עם נתונים (שם, מחיר, גודל) והתנהגות (price). |
| גודל = Attribute עם כלל | self.size, עם ולידציה ב-__init__. |
| “גדול = +3” שייך למשקה | Drink.price(), כי המשקה יודע את הגודל שלו. |
| הזמנה = Class | class Order עם פריטים וסטטוס. |
| לקוח = Attribute (בינתיים) | self.customer_name, מחרוזת. |
| “ביטול רק לפני הכנה” | ולידציה ב-Order.cancel(). |
| תפריט = dict | MENU. אין לו התנהגות, ולכן אין צורך ב-Class. |
| סכום = מתודה | total() מחשב מתוך drinks, ולא נשמר כנתון שעלול להתיישן. |
דוגמה נוספת
כשהדרישה משתנה — לקוח הופך ל-Class. נוספה דרישה: “ללקוחות יש כרטיס מועדון: כל קנייה צוברת נקודה לכל 10 ש"ח, וב-50 נקודות מקבלים משקה חינם.”
עכשיו ללקוח יש נתונים (נקודות), התנהגות (צבירה, מימוש) וכללים (50 נקודות). לפי ארבע השאלות, הוא כבר Class:
POINTS_PER_SHEKEL = 0.1
FREE_DRINK_POINTS = 50
class Customer:
def __init__(self, name):
self.name = name
self.points = 0
def earn_points(self, amount_paid):
self.points = self.points + int(amount_paid * POINTS_PER_SHEKEL)
def has_free_drink(self):
return self.points >= FREE_DRINK_POINTS
def redeem_free_drink(self):
if not self.has_free_drink():
raise ValueError(self.name + " has only " + str(self.points) + " points")
self.points = self.points - FREE_DRINK_POINTS
dana = Customer("Dana")
for payment in [120, 260, 140]:
dana.earn_points(payment)
print(dana.name, "has", dana.points, "points. Free drink?", dana.has_free_drink())
dana.redeem_free_drink()
print("After redeeming:", dana.points)פלט:
Dana has 52 points. Free drink? True
After redeeming: 2ובמבנה: Order מחזיקה עכשיו self.customer (אובייקט Customer) במקום customer_name, וכשהזמנה משולמת, היא קוראת ל-self.customer.earn_points(self.total()). אותו מושג, “לקוח”, התחיל כ-Attribute והפך ל-Class כשהדרישות גדלו. זה טבעי, וזה תהליך העבודה הנכון.
סיכום
- שמות עצם ← מועמדים ל-Classes/Attributes. פעלים ← Methods. כללים ← Validation.
- Class כשיש זהות + נתונים, התנהגות או כללים, וכמה מופעים. ערך בודד בלי התנהגות ← Attribute.
- אותו מושג יכול להיות Attribute במערכת אחת ו-Class באחרת. מתחילים פשוט, ומשדרגים כשהדרישות גדלות.
- Method שייכת ל-Class שמחזיקה את הנתונים שהיא צריכה (Information Expert).
- לפעמים “פעולה” (השאלה, הזמנה) היא בעצם Class, כשיש לה נתונים ולוגיקה משלה.
- לא Class לכל שם עצם, ולא God Class שעושה הכול.
דוקומנטציה רשמית
- Random Remarks — מוסכמות לכתיבת מחלקות
פרק שביעי: חוזרים לבנק — הכול ביחד
מה נלמד?
חוזרים למערכת הבנק מתחילת השיעור, ובונים אותה מחדש — הפעם עם כל מה שלמדנו בפרקים הקודמים.
למה צריך את זה?
כל פרק הוסיף כלי אחד ופתר בעיה אחת. כאן רואים איך כולם עובדים יחד ב-Class אחת — וזה בדיוק מה שתעשו לבד במיני פרויקט.
הבנק — עם כל מה שלמדנו
class BankAccount:
accounts_opened = 0 # shared by all accounts
def __init__(self, owner, balance=0):
if balance < 0:
raise ValueError("balance cannot be negative")
self.owner = owner # per account
self.balance = balance
BankAccount.accounts_opened = BankAccount.accounts_opened + 1
def deposit(self, amount): # setter - changes the state
if amount <= 0:
raise ValueError("amount must be positive")
self.balance = self.balance + amount
def __str__(self):
return self.owner + ": " + str(self.balance)
dana = BankAccount("Dana", 1000)
dana.deposit(200)
print(dana) # Dana: 1200| חלק בקוד | מאיזה פרק |
|---|---|
class BankAccount — תבנית אחת לכל החשבונות | פרק שני |
__init__ עם בדיקה — כל חשבון נולד שלם ותקין | פרק שלישי |
deposit — Setter ששומר על הכללים | פרק רביעי |
__str__ — איך החשבון נראה ב-print | פרק רביעי |
accounts_opened — מונה משותף לכל החשבונות | פרק חמישי |
ארבע הבעיות מהפרק הראשון נפתרו: אי אפשר ליצור חשבון עם יתרה שלילית, הנתונים והפעולות יושבים יחד, למבנה יש הגדרה אחת, ואת הכללים אוכפת ה-Class עצמה.
סיכום
- Class היא תבנית, ו-Object הוא מופע שנבנה ממנה — עם נתונים משלו.
__init__רץ ביצירה ומבטיח שהאובייקט נולד שלם;selfהוא האובייקט הנוכחי.- Methods הן הפעולות: Getter מחזיר מידע, Setter משנה את המצב ובודק את הכללים, ו-
__str__קובע מהprintמציג. - Class Attribute הוא נתון אחד שמשותף לכל האובייקטים — כמו מונה.
דוקומנטציה רשמית
- Python Tutorial — Classes — חזרה על כל הפרק
תרגול
התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.
מה מתרגלים בדף הזה: למה צריך OOP, יצירת Class עם __init__ ובדיקת ערכים, מתודות (Getter ו-Setter) ו-__str__, נתון משותף לכל ה-Class, ותכנון — מה Class, מה Attribute ומה Method.
בסוף הדף תדעו:
- לזהות מתי עדיף לאחד נתונים ופעולות לאובייקט אחד
- לכתוב Class שלמה:
__init__, מתודות ו-__str__ - להשתמש ב-Class Attribute כמונה משותף לכל האובייקטים
- להפוך תיאור מילולי לרשימה של Classes, Attributes ו-Methods
תרגיל 1: זהו את הבעיות
מה עושים: קוראים קוד קיים ומוצאים בו בעיות — בלי לכתוב קוד.
הקוד:
student1 = {"name": "Dana", "grades": [90, 85]}
student2 = {"name": "Avi", "grades": [70]}
student3 = {"nmae": "Noa", "grades": []}
def average(student):
return sum(student["grades"]) / len(student["grades"])המשימה: מצאו שלוש בעיות שנובעות מכך שהנתונים (המילונים) והפעולות (הפונקציה) נמצאים בנפרד. לכל בעיה כתבו משפט אחד: מה הבעיה, ובאיזו שורה היא מופיעה.
מה מגישים: שלוש בעיות, משפט אחד לכל אחת.
הצג פתרון
- שגיאת הקלדה יוצרת מפתח חדש — ל-
student3יש"nmae"במקום"name". אין שום מבנה מוגדר שיתפוס את זה, והבעיה תתגלה רק כשמישהו ינסהstudent3["name"]. - אין כללים — כל קוד יכול לשים ב-
gradesגם-50או"abc", ואף אחד לא בודק. - הנתונים והפעולות מפוזרים —
averageהיא פונקציה נפרדת שמקבלת מילון, ולא יודעת אם הוא בכלל סטודנט. ועם רשימה ריקה (student3) היא תיפול ב-ZeroDivisionError.
ב-OOP, Student מגדיר פעם אחת איזה נתונים יש לו ואילו פעולות הוא יודע לעשות — כולם יחד.
תרגיל 2: בדיקה ביצירה
מה בונים: מוצר בחנות — שאי אפשר ליצור עם מחיר שלילי.
מה לכתוב:
- Class בשם
Product. __init__שמקבל שם, מחיר וכמות. אם הכמות לא נשלחה — היא 0.- אם המחיר שלילי: זורקים
ValueErrorעם ההודעה שמופיעה בפלט, ולא שומרים כלום. - אחרת: שומרים את שלושת הנתונים על האובייקט, עם
self.
קוד לבדיקה:
pen = Product("Pen", 5)
print(pen.name, pen.price, pen.quantity)
try:
Product("Bad", -10)
except ValueError as error:
print("error:", error)הפלט הרצוי:
Pen 5 0
error: price cannot be negativeהצג פתרון
class Product:
def __init__(self, name, price, quantity=0):
if price < 0:
raise ValueError("price cannot be negative")
self.name = name
self.price = price
self.quantity = quantity
pen = Product("Pen", 5)
print(pen.name, pen.price, pen.quantity)
try:
Product("Bad", -10)
except ValueError as error:
print("error:", error)Pen 5 0
error: price cannot be negativeהבדיקה ב-__init__ מבטיחה שאין אף פעם אובייקט Product עם מחיר לא חוקי.
תרגיל 3: Student עם מתודות ו-str
מה בונים: סטודנט שמנהל את הציונים שלו בעצמו.
מה לכתוב:
- Class בשם
Student. ה-__init__מקבל שם ויוצר רשימת ציונים ריקה. add_grade— מקבלת ציון ומוסיפה אותו לרשימה. זו Setter: היא משנה את המצב.average— מחזירה את הממוצע, או 0 אם אין עדיין ציונים. זו Getter: היא רק מחזירה.__str__— מחזירה את השם ואת הממוצע, בדיוק כמו בשורה האחרונה של הפלט.
קוד לבדיקה:
dana = Student("Dana")
print(dana.average())
dana.add_grade(90)
dana.add_grade(75)
print(dana.average())
print(dana)הפלט הרצוי:
0
82.5
Dana (average 82.5)הצג פתרון
class Student:
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
self.grades.append(grade)
def average(self):
if len(self.grades) == 0:
return 0
return sum(self.grades) / len(self.grades)
def __str__(self):
return self.name + " (average " + str(self.average()) + ")"
dana = Student("Dana")
print(dana.average())
dana.add_grade(90)
dana.add_grade(75)
print(dana.average())
print(dana)0
82.5
Dana (average 82.5)add_grade היא Setter — משנה את המצב. average היא Getter — מחזירה מידע בלי לשנות. print קורא ל-__str__ אוטומטית.
תרגיל 4: מונה משותף
מה בונים: מוצר שיודע את המחיר שלו כולל מע"מ — ו-Class שסופרת כמה מוצרים נוצרו.
מה לכתוב:
- Class בשם
Product, עם__init__שמקבל שם ומחיר. - שני Class Attributes — נתונים ששייכים לכל ה-Class, לא לאובייקט אחד:
VAT_RATEשערכו 0.17, ו-created_countשמתחיל ב-0. - בכל יצירה של מוצר מגדילים את
created_countב-1. price_with_vat— מחזירה את המחיר כולל מע"מ.
קוד לבדיקה:
laptop = Product("Laptop", 1000)
mouse = Product("Mouse", 50)
print(Product.created_count)
print(laptop.price_with_vat())הפלט הרצוי:
2
1170.0הצג פתרון
class Product:
VAT_RATE = 0.17
created_count = 0
def __init__(self, name, price):
self.name = name
self.price = price
Product.created_count = Product.created_count + 1
def price_with_vat(self):
return round(self.price * (1 + Product.VAT_RATE), 2)
laptop = Product("Laptop", 1000)
mouse = Product("Mouse", 50)
print(Product.created_count)
print(laptop.price_with_vat())2
1170.0nameו-priceשייכים לכל מוצר בנפרד — Instance Attributes.VAT_RATEו-created_countמשותפים לכל המוצרים — Class Attributes.- המונה מתעדכן דרך
Product.created_count— לא דרךself.
תרגיל 5: שמות עצם ופעלים
מה עושים: תכנון בלבד, בלי קוד — הופכים תיאור מילולי לרשימה של Classes, Attributes ו-Methods.
הדרישה:
“בבית קפה, לקוחות מזמינים משקאות בגדלים שונים. לכל משקה יש מחיר בסיס, ומשקה גדול עולה 3 ש"ח יותר. הלקוח צובר נקודות על כל הזמנה, ויכול לממש אותן.”
המשימה: בנו טבלה עם שלוש עמודות: המועמד, מה הוא (Class, Attribute או Method), ולמה.
רמז: שמות עצם הם מועמדים ל-Classes או ל-Attributes. פעלים הם מועמדים ל-Methods.
מה מגישים: טבלה עם כל המועמדים מהדרישה.
הצג פתרון
| מועמד | מה הוא | למה |
|---|---|---|
לקוח (Customer) | Class | שם עצם עם נתונים (שם, נקודות) והתנהגות (צובר, מממש) |
משקה (Drink) | Class | שם עצם עם נתונים (מחיר בסיס, גודל) וכלל (גדול עולה יותר) |
| מחיר בסיס, גודל | Attributes של Drink | ערכים פשוטים שמתארים משקה |
| נקודות | Attribute של Customer | מספר פשוט |
| צובר נקודות, מממש | Methods של Customer | פעלים — פעולות על הנתונים של הלקוח |
| מחיר סופי | Method של Drink | מחושב מהמחיר והגודל |
מיני פרויקט: ספרייה קטנה
איזו מערכת בונים: מערכת קטנה לספרייה. בספרייה יש ספרים, ולכל ספר כמה עותקים. קוראים משאילים עותקים ומחזירים אותם, והמערכת יודעת בכל רגע כמה עותקים פנויים מכל ספר — וכמה ספרים יש בספרייה בסך הכול.
מה תלמדו מהפרויקט:
- לבנות Class שלמה מאפס:
__init__, מתודות ו-__str__ - לבדוק ערכים כבר ביצירה, ולזרוק שגיאה כשהם לא חוקיים
- להבחין בין Setter שמשנה את המצב לבין Getter שרק מחזיר מידע
- להשתמש ב-Class Attribute כמונה משותף לכל האובייקטים
מה צריך לבנות — שלב אחרי שלב:
שלב 1 — ה-Class והמונה המשותף. כתבו Class בשם Book, עם Class Attribute בשם total_books שמתחיל ב-0. הוא יספור כמה ספרים נוצרו.
שלב 2 — יצירת ספר. ה-__init__ מקבל שם, סופר ומספר עותקים, ושומר אותם. בהתחלה כל העותקים פנויים — שומרים אותם גם ב-available. כל ספר חדש מגדיל את total_books ב-1. אם מספר העותקים קטן מ-1 — זורקים ValueError, ולא יוצרים ספר.
שלב 3 — השאלה והחזרה (Setters).
borrow— מורידה עותק פנוי אחד. אם אין עותקים פנויים:ValueError.return_book— מחזירה עותק אחד. אם כל העותקים כבר בספרייה:ValueError.
שלב 4 — בדיקה (Getter). is_available מחזירה True אם יש לפחות עותק פנוי אחד, אחרת False.
שלב 5 — הצגה. __str__ מחזירה שם, סופר וכמה עותקים פנויים מתוך כמה, כמו בפלט.
הודעות השגיאה — בדיוק כך:
copies must be at least 1
no copies available
all copies are already inקוד לבדיקה:
books = [
Book("Clean Code", "Martin", 3),
Book("Python Basics", "Cohen", 1),
]
books[0].borrow()
books[1].borrow()
try:
books[1].borrow()
except ValueError as error:
print("Python Basics ->", error)
try:
Book("Empty", "Nobody", 0)
except ValueError as error:
print("Empty ->", error)
for book in books:
print(book, "| available:", book.is_available())
print("Books in the library:", Book.total_books)הפלט הרצוי:
Python Basics -> no copies available
Empty -> copies must be at least 1
Clean Code by Martin (2/3 available) | available: True
Python Basics by Cohen (0/1 available) | available: False
Books in the library: 2בסוף הפרויקט תדעו:
- לתרגם תיאור של “דבר” בעולם — ספר — ל-Class עם נתונים ופעולות
- לשמור על כללים בתוך האובייקט, כך שאי אפשר להגיע למצב לא חוקי
- להבין למה הספר Empty לא נספר במונה: השגיאה נזרקה לפני ההגדלה
בונוס: הוסיפו Class בשם Library שמחזיקה רשימת ספרים, עם add_book שמוסיפה ספר, ו-available_books שמחזירה רק את הספרים שאפשר להשאיל עכשיו.
הצג פתרון
class Book:
total_books = 0
def __init__(self, title, author, copies):
if copies < 1:
raise ValueError("copies must be at least 1")
self.title = title
self.author = author
self.copies = copies
self.available = copies
Book.total_books = Book.total_books + 1
def borrow(self):
if self.available == 0:
raise ValueError("no copies available")
self.available = self.available - 1
def return_book(self):
if self.available == self.copies:
raise ValueError("all copies are already in")
self.available = self.available + 1
def is_available(self):
return self.available > 0
def __str__(self):
return (self.title + " by " + self.author + " ("
+ str(self.available) + "/" + str(self.copies) + " available)")
books = [
Book("Clean Code", "Martin", 3),
Book("Python Basics", "Cohen", 1),
]
books[0].borrow()
books[1].borrow()
try:
books[1].borrow()
except ValueError as error:
print("Python Basics ->", error)
try:
Book("Empty", "Nobody", 0)
except ValueError as error:
print("Empty ->", error)
for book in books:
print(book, "| available:", book.is_available())
print("Books in the library:", Book.total_books)Python Basics -> no copies available
Empty -> copies must be at least 1
Clean Code by Martin (2/3 available) | available: True
Python Basics by Cohen (0/1 available) | available: False
Books in the library: 2total_booksהוא Class Attribute — מונה אחד לכל הספרים. הספרEmptyלא נוצר (השגיאה נזרקה לפני ההגדלה), ולכן המונה הוא 2.borrowו-return_bookהם Setters — משנים את המצב, ובודקים את הכללים לפני השינוי.is_availableהוא Getter — מחזיר מידע בלי לשנות כלום.
בונוס:
class Library:
def __init__(self):
self.books = []
def add_book(self, book):
self.books.append(book)
def available_books(self):
result = []
for book in self.books:
if book.is_available():
result.append(book)
return result
library = Library()
for book in books:
library.add_book(book)
for book in library.available_books():
print(book)Clean Code by Martin (2/3 available)