מחלקות ואובייקטים: מהבעיה ועד Class שלמה

מטרות השיעור
  • לזהות את הבעיות של קוד פרוצדורלי כשהמערכת גדלה, ולהסביר איך OOP פותר אותן
  • להגדיר Class וליצור ממנה כמה אובייקטים, ולהבחין בין Class ל-Object
  • לכתוב __init__ שמאתחל אובייקט שלם ובודק את הערכים, ולהסביר מה זה self
  • להוסיף Methods — Getters ו-Setters — ו-__str__ שמציג את האובייקט
  • להבחין בין Instance Attribute ל-Class Attribute, ולהשתמש במונה משותף
  • להפוך תיאור מילולי למבנה מחלקות — מה Class, מה Attribute ומה Method
  • לבנות Class שלמה שמחברת את כל הרעיונות — במיני פרויקט

מה בשיעור

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

  1. פרק ראשון — למה צריך OOP: הבעיות של קוד פרוצדורלי
  2. פרק שני — Class ו-Object: תבנית ומופעים
  3. פרק שלישי — __init__ ו-self: אובייקט שנולד שלם
  4. פרק רביעי — Methods: האובייקט לא רק יודע, הוא גם עושה
  5. פרק חמישי — נתונים משותפים: Class Attributes
  6. פרק שישי — מה הופך ל-Class: מדרישה מילולית למחלקות
  7. פרק שביעי — חוזרים לבנק: הכול ביחד

בסוף השיעור — תרגול, ומיני פרויקט שמחבר את כל הפרקים.

פרק ראשון: למה צריך 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

הבעיות:

  1. student3 נוצר עם שגיאת כתיב ("nmae"). אין שום מקום שמגדיר מה “חייב” להיות בסטודנט.
  2. student2["grades"].append(500) עקף את add_grade. גם אילו add_grade הייתה בודקת שהציון בין 0 ל-100, הבדיקה לא הייתה רצה. ממוצע 285!
  3. average קורסת על סטודנט בלי ציונים. ומי שמשתמש ב-dict של סטודנט צריך לדעת שהפונקציה הזו קיימת, ולזכור לטפל במקרה הזה.

ב-OOP, Student תהיה Class אחת עם מבנה מוגדר (שם וציונים, תמיד), עם מתודה add_grade שהיא הדרך היחידה להוסיף ציון (ובודקת אותו), ועם מתודה average שיודעת לטפל ברשימה ריקה. הכללים יושבים ליד הנתונים שהם שומרים עליהם.

סיכום

  • תכנות פרוצדורלי: נתונים (dicts) ופונקציות בנפרד. עובד, אבל כשהמערכת גדלה, הנתונים חשופים, הפעולות מפוזרות, ואין מבנה מחייב.
  • OOP: מארגנים את הקוד סביב דברים. כל אובייקט מחזיק נתונים (Attributes) ופעולות (Methods) יחד.
  • Class = תבנית (תוכנית בנייה, תבנית ייצור). Object = דבר ספציפי שנוצר ממנה.
  • ב-Python כל ערך הוא אובייקט: str ו-list הן Classes, ו-"hi".upper() היא קריאה למתודה.
  • OOP הוא דרך חשיבה: אילו דברים יש, מה הם יודעים, ומה הם עושים. לא כל דבר צריך להיות Class.

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

פרק שני: 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.

ClassObject / 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 aviFalse: לא אותו אובייקט (נושא 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__.

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

פרק שלישי: __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)

  1. Python יוצרת אובייקט ריק חדש מסוג Student בזיכרון.
  2. Python קוראת ל-__init__ ומעבירה לו: self = האובייקט החדש, name = "Dana", age = 24.
  3. __init__ מריץ self.name = name, כלומר name של האובייקט החדש = "Dana". וכך גם age.
  4. __init__ מסתיים. הפרמטרים המקומיים נעלמים, וה-Attributes נשארים על האובייקט.
  5. 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 = nameAttribute 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, כך שאובייקט לא חוקי לא נוצר בכלל.

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

פרק רביעי: 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 שעלול להתיישן.

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

פרק חמישי: נתונים משותפים — 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?

AttributeMethod
מה זהנתון, מה האובייקט יודע / מחזיקפעולה, מה האובייקט עושה / מחשב
חלק דיברשם עצם / תוארפועל
דוגמאותname, price, balance, is_activedeposit(), add_grade(), total()
גישהobj.priceobj.total() עם סוגריים

שאלות שעוזרות להחליט:

  1. האם זה נתון שנשמר, או משהו שמחושב? נתון שנשמר ‏←‏ Attribute. מחושב מנתונים אחרים ‏←‏ בדרך כלל Method (ראינו: description שהתיישן).
  2. האם זה משנה את האובייקט? כל שינוי מסודר ‏←‏ Method (פקודה).
  3. האם צריך כללים כשמשנים אותו? אם כן, השינוי עובר דרך 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 מחפשת:

  1. קודם על האובייקט (Instance Attributes).
  2. אם לא נמצא, על ה-Class (Class Attributes ומתודות).
  3. לא נמצא ‏←‏ 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.17Class Attributeערך אחד לכל המוצרים. אותיות גדולות = “קבוע” (מוסכמה).
created_count = 0Class Attributeמונה משותף. מתחיל ב-0 כשה-Class מוגדרת.
self.name, self.priceInstance 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 Attribute student_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 — מדרישה מילולית למחלקות

מה נלמד?

עד עכשיו אמרנו לכם אילו 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

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

החלטת תכנוןאיך היא נראית בקוד
משקה = Classclass Drink עם נתונים (שם, מחיר, גודל) והתנהגות (price).
גודל = Attribute עם כללself.size, עם ולידציה ב-__init__.
“גדול = +3” שייך למשקהDrink.price(), כי המשקה יודע את הגודל שלו.
הזמנה = Classclass Order עם פריטים וסטטוס.
לקוח = Attribute (בינתיים)self.customer_name, מחרוזת.
“ביטול רק לפני הכנה”ולידציה ב-Order.cancel().
תפריט = dictMENU. אין לו התנהגות, ולכן אין צורך ב-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 שעושה הכול.

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

פרק שביעי: חוזרים לבנק — הכול ביחד

מה נלמד?

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

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

כל פרק הוסיף כלי אחד ופתר בעיה אחת. כאן רואים איך כולם עובדים יחד ב-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 הוא נתון אחד שמשותף לכל האובייקטים — כמו מונה.

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

תרגול

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

מה מתרגלים בדף הזה: למה צריך 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"])

המשימה: מצאו שלוש בעיות שנובעות מכך שהנתונים (המילונים) והפעולות (הפונקציה) נמצאים בנפרד. לכל בעיה כתבו משפט אחד: מה הבעיה, ובאיזו שורה היא מופיעה.

מה מגישים: שלוש בעיות, משפט אחד לכל אחת.

הצג פתרון
  1. שגיאת הקלדה יוצרת מפתח חדש — ל-student3 יש "nmae" במקום "name". אין שום מבנה מוגדר שיתפוס את זה, והבעיה תתגלה רק כשמישהו ינסה student3["name"].
  2. אין כללים — כל קוד יכול לשים ב-grades גם -50 או "abc", ואף אחד לא בודק.
  3. הנתונים והפעולות מפוזרים — average היא פונקציה נפרדת שמקבלת מילון, ולא יודעת אם הוא בכלל סטודנט. ועם רשימה ריקה (student3) היא תיפול ב-ZeroDivisionError.

ב-OOP, Student מגדיר פעם אחת איזה נתונים יש לו ואילו פעולות הוא יודע לעשות — כולם יחד.

תרגיל 2: בדיקה ביצירה

מה בונים: מוצר בחנות — שאי אפשר ליצור עם מחיר שלילי.

מה לכתוב:

  1. Class בשם Product.
  2. __init__ שמקבל שם, מחיר וכמות. אם הכמות לא נשלחה — היא 0.
  3. אם המחיר שלילי: זורקים ValueError עם ההודעה שמופיעה בפלט, ולא שומרים כלום.
  4. אחרת: שומרים את שלושת הנתונים על האובייקט, עם 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

מה בונים: סטודנט שמנהל את הציונים שלו בעצמו.

מה לכתוב:

  1. Class בשם Student. ה-__init__ מקבל שם ויוצר רשימת ציונים ריקה.
  2. add_grade — מקבלת ציון ומוסיפה אותו לרשימה. זו Setter: היא משנה את המצב.
  3. average — מחזירה את הממוצע, או 0 אם אין עדיין ציונים. זו Getter: היא רק מחזירה.
  4. __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 שסופרת כמה מוצרים נוצרו.

מה לכתוב:

  1. Class בשם Product, עם __init__ שמקבל שם ומחיר.
  2. שני Class Attributes — נתונים ששייכים לכל ה-Class, לא לאובייקט אחד: VAT_RATE שערכו 0.17, ו-created_count שמתחיל ב-0.
  3. בכל יצירה של מוצר מגדילים את created_count ב-1.
  4. 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.0
  • name ו-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: 2
  • total_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)