מחלקות

הקדמה

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

לכל משתמש יש את התכונות הבאות:

  • שם פרטי
  • שם משפחה
  • כינוי
  • גיל

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

חשוב!
פתרו לפני שתמשיכו!

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

נמצא עוד דוגמאות לאסופות תכונות שכאלו:

  • תכונותיו של שולחן הן גובה, מספר רגליים, צבע, אורך ורוחב.
  • תכונותיה של נורה הן צבע ומצב (דולקת או כבויה).
  • תכונותיו של תרגיל בקורס הן השבוע והמחברת שבהן הוא הופיע, כותרת והוראות התרגיל.
  • תכונותיו של שיר הן מילות השיר, האומנים שהשתתפו ביצירתו ואורכו.

חשבו על עוד 3 דוגמאות לעצמים שאפשר לתאר כערכים עם אסופת תכונות.

חשוב!
פתרו לפני שתמשיכו!

ניצור שני משתמשים לדוגמה לפי תכונותיהם שהוצגו לעיל:


In [ ]:
user1 = {
    'first_name': 'Christine',
    'last_name': 'Daaé',
    'nickname': 'Little Lotte',
    'age': 20,
}
user2 = {
    'first_name': 'Elphaba',
    'last_name': 'Thropp',
    'nickname': 'Elphie',
    'age': 19,
}

תוכלו ליצור בעצמכם פונקציה שיוצרת משתמש חדש?
זה לא מסובך מדי:


In [ ]:
def create_user(first_name, last_name, nickname, current_age):
    return {
        'first_name': first_name,
        'last_name': last_name,
        'nickname': nickname,
        'age': current_age,
    }


# נקרא לפונקציה כדי לראות שהכל עובד כמצופה
new_user = create_user('Bayta', 'Darell', 'Bay', 24)
print(new_user)

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


In [ ]:
def describe_as_a_string(user):
    first_name = user['first_name']
    last_name = user['last_name']
    full_name = f'{first_name} {last_name}'
    nickname = user['nickname']
    age = user['age']
    return f'{nickname} ({full_name}) is {age} years old.'


def celebrate_birthday(user):
    user['age'] = user['age'] + 1


print(describe_as_a_string(new_user))
celebrate_birthday(new_user)
print("--- After birthday")
print(describe_as_a_string(new_user))

הצלחנו לערוך את ערכו של user['age'] מבלי להחזיר ערך, כיוון שמילונים הם mutable.
אם זה נראה לכם מוזר, חזרו למחברת על mutability ו־immutability.

בשלב הזה בתוכניתנו קיימות קבוצת פונקציות שמטרתן היא ניהול של משתמשים ושל תכונותיהם.

נוכל להוסיף למשתמש תכונות נוספות, כמו דוא"ל ומשקל, לדוגמה,
או להוסיף לו פעולות שיהיה אפשר לבצע עליו, כמו הפעולה eat_bourekas, שמוסיפה לתכונת המשקל של המשתמש חצי קילו.

חסרונות

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

הבעיה תצוף כשנרצה להוסיף לתוכנה שלנו עוד מבנים שכאלו.
לדוגמה, כשנרצה להוסיף לצ'יקצ'וק יכולת לניהול סרטונים – שתכונותיהם אורך סרטון ומספר לייקים, והפעולה עליהם היא היכולת לעשות Like לסרטון.
הקוד לניהול המשתמש והקוד לניהול הסרטונים עלולים להתערבב, יווצרו תלויות ביניהם וחוויית ההתמצאות בקוד תהפוך ללא נעימה בעליל.

החוסר באיגוד התכונות והפונקציות אף מקשה על הקורא להבין לאן שייכות כל אחת מהתכונות והפונקציות, ומה תפקידן בקוד.
מי שמסתכל על הקוד שלנו לא יכול להבין מייד ש־describe_as_a_string מיועדת לפעול רק על מבנים שנוצרו מ־create_user.
הוא עלול לנסות להכניס מבנים אחרים ולהקריס את התוכנית, או גרוע מכך – להיתקל בבאגים בעתיד, בעקבות שימוש לא נכון בפונקציה.

הגדרה

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

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

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

איור המתאר את התכונות ואת הפעולות השייכות למחלקה "משתמש".

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

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

יצירת מחלקות

מחלקה בסיסית

ראשית, ניצור את המחלקה הפשוטה ביותר שאנחנו יכולים לבנות, ונקרא לה User.
בהמשך המחברת נרחיב את המחלקה, והיא תהיה זו שמטפלת בכל הקשור במשתמשים של צ'יקצ'וק:


In [ ]:
class User:
    pass

ניסינו ליצור את המבנה הכי קצר שאפשר, אבל class חייב להכיל קוד.
כדי לעקוף את המגבלה הזו, השתמשנו במילת המפתח pass, שאומרת לפייתון "אל תעשי כלום".

בקוד שלמעלה השתמשנו במילת המפתח class כדי להצהיר על מחלקה חדשה.
מייד לאחר מכן ציינו את שם המחלקה שאנחנו רוצים ליצור – User במקרה שלנו.
שם המחלקה נתון לחלוטין לבחירתנו, והמילה User לא אומרת לפייתון שום דבר מיוחד. באותה המידה יכולנו לבחור כל שם אחר.

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


In [ ]:
user1 = User()

כעת יצרנו משתמש, ואנחנו יכולים לשנות את התכונות שלו.
מבחינה מילולית, נהוג להגיד שיצרנו מופע (Instance) או עצם (אובייקט, Object) מסוג User, ששמו user1.
השתמשנו לשם כך במחלקה בשם User.

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


In [ ]:
user1.first_name = "Miles"
user1.last_name = "Prower"
user1.age = 8
user1.nickname = "Tails"

נוכל לאחזר את התכונות הללו בקלות, באותה הצורה:


In [ ]:
print(user1.age)

ואם נבדוק מה הסוג של המשתנה user1, מצפה לנו הפתעה נחמדה:


In [ ]:
type(user1)

איזה יופי! המחלקה גרמה לכך ש־User הוא ממש סוג משתנה בפייתון עכשיו.
קחו לעצמכם רגע להתפעל – יצרנו סוג משתנה חדש בפייתון!
אם כך, המשתנה user1 מצביע על מופע של משתמש, שסוגו User.

ננסה ליצור מופע נוסף, הפעם של משתמש אחר:


In [ ]:
user2 = User()
user2.first_name = "Harry"
user2.last_name = "Potter"
user2.age = 39
user2.nickname = "BoyWhoLived1980"

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


In [ ]:
print(f"{user1.first_name} {user1.last_name} is {user1.age} years old.")
print(f"{user2.first_name} {user2.last_name} is {user2.age} years old.")

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

צרו מחלקה בשם Point שמייצגת נקודה.
צרו 2 מופעים של נקודות: אחת בעלת x שערכו 3 ו־y שערכו 1, והשנייה בעלת x שערכו 4 ו־y שערכו 1.

חשוב!
פתרו לפני שתמשיכו!

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

מחלקה עם פעולות

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


In [ ]:
def describe_as_a_string(user):
    full_name = f'{user.first_name} {user.last_name}'
    return f'{user.nickname} ({full_name}) is {user.age} years old.'


print(describe_as_a_string(user2))

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


In [ ]:
class User:
    def describe_as_a_string(user):
        full_name = f'{user.first_name} {user.last_name}'
        return f'{user.nickname} ({full_name}) is {user.age} years old.'


user3 = User()
user3.first_name = "Anthony John"
user3.last_name = "Soprano"
user3.age = 61
user3.nickname = "Tony"

בתא שלמעלה הגדרנו את הפונקציה describe_as_a_string בתוך המחלקה User.
פונקציה שמוגדרת בתוך מחלקה נקראת פעולה (Method), שם שניתן לה כדי לבדל אותה מילולית מפונקציה רגילה.

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


In [ ]:
user3.describe_as_a_string()

חדי העין שמו ודאי לב למשהו מעט משונה בקריאה לפעולה describe_as_a_string.
הפעולה מצפה לקבל פרמטר (קראנו לו user), אבל כשקראנו לה בתא האחרון לא העברנו לה אף ארגומנט!

זהו קסם ידוע ונחמד של מחלקות: כשמופע קורא לפעולה כלשהי – אותו מופע עצמו מועבר אוטומטית כארגומנט הראשון לפעולה.
לדוגמה, בקריאה user3.describe_as_a_string(), המופע user3 הועבר לתוך הפרמטר user של describe_as_a_string.

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


In [ ]:
class User:
    def describe_as_a_string(self):
        full_name = f'{self.first_name} {self.last_name}'
        return f'{self.nickname} ({full_name}) is {self.age} years old.'


user3 = User()
user3.first_name = "Anthony John"
user3.last_name = "Soprano"
user3.age = 61
user3.nickname = "Tony"
user3.describe_as_a_string()

טעות נפוצה היא לשכוח לשים self כפרמטר הראשון בפעולות שנגדיר.

צרו פעולה בשם describe_as_a_string עבור מחלקת Point שיצרתם.
הפעולה תחזיר מחרוזת בצורת (x, y).

חשוב!
פתרו לפני שתמשיכו!

יצירת מופע

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


In [ ]:
def create_user(first_name, last_name, nickname, current_age):
    user = User()
    user.first_name = first_name
    user.last_name = last_name
    user.nickname = nickname
    user.age = current_age
    return user


user4 = create_user('Daenerys', 'Targaryen', 'Mhysa', 23)
print(f"{user4.first_name} {user4.last_name} is {user4.age} years old.")

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

נעתיק את create_user לתוך מחלקת User, בשינויים קלים:

  1. לא נשכח לשים את self כפרמטר ראשון בחתימת הפעולה.
  2. כפי שראינו, פעולות במחלקה מקבלות מופע ועובדות ישירות עליו, ולכן נשמיט את השורות user = User() ו־return user.

In [ ]:
class User:
    def describe_as_a_string(self):
        full_name = f'{self.first_name} {self.last_name}'
        return f'{self.nickname} ({full_name}) is {self.age} years old.'

    def create_user(self, first_name, last_name, nickname, current_age):
        self.first_name = first_name
        self.last_name = last_name
        self.nickname = nickname
        self.age = current_age

עכשיו נוכל ליצור משתמש חדש, בצורה החביבה והמקוצרת הבאה:


In [ ]:
user4 = User()
user4.create_user('Daenerys', 'Targaryen', 'Mhysa', 23)
user4.describe_as_a_string()

תרגיל ביניים: מחלקת נקודות

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

הוסיפו את הפעולות create_point ו־distance למחלקת הנקודה שיצרתם.
הפעולה create_point תקבל כפרמטרים x ו־y, ותיצוק תוכן למופע שיצרתם.
הפעולה distance תחזיר את המרחק של מקגונגל מהוגוורטס, הממוקם בנקודה (0, 0).

נוסחת המרחק היא חיבור בין הערכים המוחלטים של נקודות ה־x וה־y.
לדוגמה:

  • המרחק מהנקודה
    x = 5, y = 3
    הוא 8.
  • המרחק מהנקודה
    x = 0, y = 3
    הוא 3.
  • המרחק מהנקודה
    x = -3, y = 3
    הוא 6.
  • המרחק מהנקודה
    x = -5, y = 0
    הוא 5.
  • המרחק מהנקודה
    x = 0, y = 0
    הוא 0.

ודאו שהתוכנית שלכם מחזירה Success! עבור הקוד הבא:


In [ ]:
current_location = Point()
current_location.create_point(5, 3)
if current_location.distance() == 8:
    print("Success!")

פעולות קסם

כדי להקל אפילו עוד יותר על המלאכה, בפייתון יש פעולות קסם (Magic Methods).
אלו פעולות עם שם מיוחד, שאם נגדיר אותן במחלקה, הן ישנו את ההתנהגות שלה או של המופעים הנוצרים בעזרתה.

הפעולה __str__

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


In [ ]:
user4 = User()
user4.create_user('Daenerys', 'Targaryen', 'Mhysa', 23)
str(user4)

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

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


In [ ]:
print(user4)

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


In [ ]:
print(user4.describe_as_a_string())

אבל יש דרך קלה עוד יותר!
ניחשתם נכון – פעולת הקסם __str__.
נחליף את השם של הפעולה describe_as_a_string, ל־__str__:


In [ ]:
class User:
    def __str__(self):
        full_name = f'{self.first_name} {self.last_name}'
        return f'{self.nickname} ({full_name}) is {self.age} years old.'

    def create_user(self, first_name, last_name, nickname, current_age):
        self.first_name = first_name
        self.last_name = last_name
        self.nickname = nickname
        self.age = current_age


user5 = User()
user5.create_user('James', 'McNulty', 'Jimmy', 49)
print(user5)

ראו איזה קסם! עכשיו המרה של כל מופע מסוג User למחרוזת היא פעולה ממש פשוטה!

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

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

הפעולה __init__

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


In [ ]:
class User:
    def __init__(self):
        print("New user has been created!")

    def __str__(self):
        full_name = f'{self.first_name} {self.last_name}'
        return f'{self.nickname} ({full_name}) is {self.age} years old.'

    def create_user(self, first_name, last_name, nickname, current_age):
        self.first_name = first_name
        self.last_name = last_name
        self.nickname = nickname
        self.age = current_age


user5 = User()
user5.create_user('Lorne', 'Malvo', 'Mick', 23)
print(user5)

בדוגמת הקוד שלמעלה הגדרנו את פעולת הקסם __init__, שתרוץ מייד כשנוצר מופע חדש.
החלטנו שברגע שייווצר מופע של משתמש, תודפס ההודעה New user has been created!.

הכיף הגדול ב־__init__ הוא היכולת שלה לקבל פרמטרים.
נוכל להעביר אליה את הארגומנטים בקריאה לשם המחלקה, בעת יצירת המופע 🤯


In [ ]:
class User:
    def __init__(self, message):
        self.creation_message = message
        print(self.creation_message)

    def __str__(self):
        full_name = f'{self.first_name} {self.last_name}'
        return f'{self.nickname} ({full_name}) is {self.age} years old.'

    def create_user(self, first_name, last_name, nickname, current_age):
        self.first_name = first_name
        self.last_name = last_name
        self.nickname = nickname
        self.age = current_age


user5 = User("New user has been created!")  # תראו איזה מגניב
user5.create_user('Lorne', 'Malvo', 'Mick', 58)
print(user5)
print(f"We still have the message: {user5.creation_message}")

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

ואם כבר יש לנו משהו שרץ כשאנחנו יוצרים את המופע... והוא יודע לקבל פרמטרים...
אתם חושבים על מה שאני חושב?
בואו נשנה את השם של create_user ל־__init__!
בצורה הזו נוכל לצקת את התכונות למופע מייד עם יצירתו, ולוותר על קריאה נפרדת לפעולה שמטרתה למלא את הערכים:


In [ ]:
class User:
    def __init__(self, first_name, last_name, nickname, current_age):
        self.first_name = first_name
        self.last_name = last_name
        self.nickname = nickname
        self.age = current_age
        print("Yayy! We have just created a new instance! :D")

    def __str__(self):
        full_name = f'{self.first_name} {self.last_name}'
        return f'{self.nickname} ({full_name}) is {self.age} years old.'


user5 = User('Lorne', 'Malvo', 'Mick', 58)
print(user5)

איגדנו את יצירת תכונות המופע תחת פעולה אחת, שרצה כשהוא נוצר.
הרעיון הנפלא הזה נפוץ מאוד בשפות תכנות שתומכות במחלקות, ומוכרת בשם פעולת אתחול (Initialization Method).
זו גם הסיבה לשם הפעולה – המילה init נגזרת מהמילה initialization, אתחול.

שפצו את מחלקת הנקודה שיצרתם, כך שתכיל __init__ ו־__str__.

חשוב!
פתרו לפני שתמשיכו!

ייצור מסחרי

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


In [ ]:
snailchat_users = [
    ['Mike', 'Shugarberg', 'Marker', 36],
    ['Hammer', 'Doorsoy', 'Tzweetz', 43],
    ['Evan', 'Spygirl', 'Odd', 30],
]

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

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

נוכל ליצור רשימת מופעים של משתמשים. לדוגמה:


In [ ]:
our_users = []
for user_details in snailchat_users:
    new_user = User(*user_details)  # Unpacking – התא הראשון עובר לפרמטר התואם, וכך גם השני, השלישי והרביעי
    our_users.append(new_user)
    print(new_user)

בקוד שלמעלה יצרנו רשימה ריקה, שאותה נמלא במשתמשים שנגנוב שנשאיל מסניילצ'אט.
נעביר את הפרטים של כל אחד מהמשתמשים המופיעים ב־snailchat_users, ל־__init__ של User,
ונצרף את המופע החדש שנוצר לתוך הרשימה החדשה שיצרנו.

עכשיו הרשימה our_users היא רשימה לכל דבר, שכוללת את כל המשתמשים החדשים שהצטרפו לרשת החברתית שלנו:


In [ ]:
print(our_users[0])
print(our_users[1])
print(our_users[2])

צרו את רשימת כל הנקודות שה־x וה־y שלהן הוא מספר שלם בין 0 ל־6.
לדוגמה, רשימת כל הנקודות שה־x וה־y שלהן הוא בין 0 ל־2 היא:
[(0, 0), (0, 1), (1, 0), (1, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]

חשוב!
פתרו לפני שתמשיכו!

טעויות נפוצות

גבולות מרחב הערכים

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


In [ ]:
class User:
    def __init__(self, first_name, last_name, nickname, current_age):
        self.first_name = first_name
        self.last_name = last_name
        self.nickname = nickname
        self.age = current_age
    
    def celebrate_birthday(self):
        age = age + 1

    def __str__(self):
        full_name = f'{self.first_name} {self.last_name}'
        return f'{self.nickname} ({full_name}) is {self.age} years old.'

ניסיון ליצור מופע של משתמש ולחגוג לו יום הולדת יגרום לשגיאה.
תוכלו לנחש מה תהיה השגיאה עוד לפני שתריצו?


In [ ]:
user6 =  User('Winston', 'Smith', 'Jeeves', 39)
user6.celebrate_birthday()

ניסינו לשנות את המשתנה age – אך הוא אינו מוגדר.
כדי לשנות את הגיל של המשתמש שיצרנו, נהיה חייבים להתייחס ל־self.age.
אם לא נציין במפורש שאנחנו רוצים לשנות את התכונה age ששייכת ל־self, פייתון לא תדע לאיזה מופע אנחנו מתכוונים.
נתקן:


In [ ]:
class User:
    def __init__(self, first_name, last_name, nickname, current_age):
        self.first_name = first_name
        self.last_name = last_name
        self.nickname = nickname
        self.age = current_age
    
    def celebrate_birthday(self):
        self.age = self.age + 1

    def __str__(self):
        full_name = f'{self.first_name} {self.last_name}'
        return f'{self.nickname} ({full_name}) is {self.age} years old.'


user6 =  User('Winston', 'Smith', 'Jeeves', 39)
print(f"User before birthday: {user6}")
user6.celebrate_birthday()
print(f"User after  birthday: {user6}")

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


In [ ]:
user6 =  User('Winston', 'Smith', 'Jeeves', 39)
print(user6)
age = 10
print(user6)

כדי לשנות את גילו של המשתמש, נצטרך להתייחס אל התכונה שלו בצורת הכתיבה שלמדנו:


In [ ]:
user6.age = 10
print(user6)

תכונה או פעולה שלא קיימות

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


In [ ]:
class Dice:
    def __init__(self, number):
        if 1 <= number <= 6:
            self.is_valid = True


dice_bag = [Dice(roll_result) for roll_result in range(7)]

יצרנו רשימת קוביות וביצענו השמה כך ש־dice_bag תצביע עליה.
כעת נדפיס את התכונה is_valid של כל אחת מהקוביות:


In [ ]:
for dice in dice_bag:
    print(dice.is_valid)

הבעיה היא שהקוביה הראשונה שיצרנו קיבלה את המספר 0.
במקרה כזה, התנאי בפעולת האתחול (__init__) לא יתקיים, והתכונה is_valid לא תוגדר.
כשהלולאה תגיע לקובייה 0 ותנסה לגשת לתכונה is_valid, נגלה שהיא לא קיימת עבור הקובייה 0, ונקבל AttributeError.

נתקן:


In [ ]:
class Dice:
    def __init__(self, number):
        self.is_valid = (1 <= number <= 6)  # לא חייבים סוגריים


dice_bag = [Dice(roll_result) for roll_result in range(7)]
for dice in dice_bag:
    print(dice.is_valid)

סיכום

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

נהוג לכנות את עולם המחלקות בשם "תכנות מונחה עצמים" (Object Oriented Programming, או OOP).
זו פרדיגמת תכנות הדוגלת ביצירת מחלקות לצורך חלוקת קוד טובה יותר,
ובתיאור עצמים מהעולם האמיתי בצורה טובה יותר, כאוספים של תכונות ופעולות.

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

פייתון תומכת הן בתכנות פרוצדורלי והן בתכנות מונחה עצמים.

מונחים

מחלקה (Class)
תבנית, או שבלונה, שמתארת אוסף של תכונות ופעולות שיש ביניהן קשר.
המחלקה מגדירה מבנה שבעזרתו נוכל ליצור בקלות עצם מוגדר, שוב ושוב.
לדוגמה: מחלקה המתארת משתמש ברשת חברתית, מחלקה המתארת כלי רכב, מחלקה המתארת נקודה במישור.
מופע (Instance)
נקרא גם עצם (Object).
ערך שנוצר על ידי מחלקה כלשהי. סוג הערך ייקבע לפי המחלקה שיצרה אותו.
הערך נוצר לפי התבנית ("השבלונה") של המחלקה שממנה הוא נוצר, ומוצמדות לו הפעולות שהוגדרו במחלקה.
המופע הוא יחידה עצמאית שעומדת בפני עצמה. לרוב מחלקה תשמש אותנו ליצירת מופעים רבים.
לדוגמה: המופע "נקודה שנמצאת ב־(5, 3)" יהיה מופע שנוצר מהמחלקה "נקודה".
תכונה (Property, Member)
ערך אופייני למופע שנוצר מהמחלקה.
משתנים השייכים למופע שנוצר מהמחלקה, ומכילים ערכים שמתארים אותו.
לדוגמה: לנקודה במישור יש ערך x וערך y. אלו 2 תכונות של הנקודה.
נוכל להחליט שתכונותיה של מחלקת מכונית יהיו צבע, דגם ויצרן.
פעולה (Method)
פונקציה שמוגדרת בגוף המחלקה.
מתארת התנהגויות אפשריות של המופע שייווצר מהמחלקה.
לדוגמה: פעולה על נקודה במישור יכולה להיות מציאת מרחקה מראשית הצירים.
פעולה על שולחן יכולה להיות "קצץ 5 סנטימטר מגובהו".
שדה (Field, Attribute)
שם כללי הנועד לתאר תכונה או פעולה.
שדות של מופע מסוים יהיו כלל התכונות והפעולות שאפשר לגשת אליהן מאותו מופע.
לדוגמה: השדות של נקודה יהיו התכונות x ו־y, והפעולה שבודקת את מרחקה מראשית הצירים.
פעולה מיוחדת (Special Method)
ידועה גם כ־dunder method (double under, קו תחתון כפול) או כ־magic method (פעולת קסם).
פעולה שהגדרתה במחלקה גורמת למחלקה או למופעים הנוצרים ממנה להתנהגות מיוחדת.
דוגמאות לפעולות שכאלו הן __init__ ו־__str__.
פעולת אתחול (Initialization Method)
פעולה שרצה עם יצירת מופע חדש מתוך מחלקה.
לרוב משתמשים בפעולה זו כדי להזין במופע ערכים התחלתיים.
תכנות מונחה עצמים (Object Oriented Programming)
פרדיגמת תכנות שמשתמשת במחלקות בקוד ככלי העיקרי להפשטה של העולם האמיתי.
בפרדיגמה זו נהוג ליצור מחלקות המייצגות תבניות של עצמים, ולאפיין את העצמים באמצעות תכונות ופעולות.
בעזרת המחלקות אפשר ליצור מופעים, שהם ייצוג של פריט בודד (עצם, אובייקט) שנוצר לפי תבנית המחלקה.

תרגיל לדוגמה

כתבו מחלקה המייצגת נתיב תקין במערכת ההפעלה חלונות.
הנתיב מחולק לחלקים באמצעות התו / או התו \.
החלק הראשון בנתיב הוא תמיד אות הכונן ואחריה נקודתיים.
החלקים שנמצאים אחרי החלק הראשון, ככל שיש כאלו, הם תיקיות וקבצים.
דוגמאות לנתיבים תקינים:

  • C:\Users\Yam\python.jpg
  • C:/Users/Yam/python.jpg
  • C:
  • C:\
  • C:/
  • C:\User/
  • D:/User/
  • C:/User

המחלקה תכלול את הפעולות הבאות:

  • אחזר את אות הכונן בעזרת הפעולה get_drive_letter.
  • אחזר את הנתיב ללא חלקו האחרון בעזרת הפעולה get_dirname.
  • אחזר את שם החלק האחרון בנתיב, בעזרת הפעולה get_basename.
  • אחזר את סיומת הקובץ בעזרת הפעולה get_extension.
  • אחזר אם הנתיב קיים במחשב בעזרת הפעולה is_exists.
  • אחזר את הנתיב כולו כמחרוזת, כשהתו המפריד הוא /, וללא / בסוף הנתיב.

In [ ]:
import os


class Path:
    def __init__(self, path):
        self.fullpath = path
        self.parts = list(self.get_parts())

    def get_parts(self):
        current_part = ""
        for char in self.fullpath:
            if char in r"\/":
                yield current_part
                current_part = ""
            else:
                current_part = current_part + char
        if current_part != "":
            yield current_part

    def get_drive_letter(self):
        return self.parts[0].rstrip(":")

    def get_dirname(self):
        path = "/".join(self.parts[:-1])
        return Path(path)

    def get_basename(self):
        return self.parts[-1]

    def get_extension(self):
        name = self.get_basename()
        i = name.rfind('.')
        if 0 < i < len(name) - 1:
            return name[i + 1:]
        return ''

    def is_exists(self):
        return os.path.exists(str(self))

    def normalize_path(self):
        normalized = "\\".join(self.parts)
        return normalized.rstrip("\\")

    def info_message(self):
        return f"""
            Some info about "{self}":
            Drive letter: {self.get_drive_letter()}
            Dirname: {self.get_dirname()}
            Last part of path: {self.get_basename()}
            File extension: {self.get_extension()}
            Is exists?: {self.is_exists()}
        """.strip()

    def __str__(self):
        return self.normalize_path()


EXAMPLES = (
    r"C:\Users\Yam\python.jpg",
    r"C:/Users/Yam/python.jpg",
    r"C:",
    r"C:\\",
    r"C:/",
    r"C:\Users/",
    r"D:/Users/",
    r"C:/Users",
)
for example in EXAMPLES:
    path = Path(example)
    print(path.info_message())
    print()

תרגילים

סקרנות

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

  1. vote שמקבלת כפרמטר אפשרות הצבעה לסקר ומגדילה את מספר ההצבעות בו ב־1.
  2. add_option, שמקבלת כפרמטר אפשרות הצבעה לסקר ומוסיפה אותה.
  3. remove_option שמקבלת כפרמטר אפשרות הצבעה לסקר ומוחקת אותה.
  4. get_votes המחזירה את כל האפשרויות כרשימה של tuple, המסודרים לפי כמות ההצבעות.
    בכל tuple התא הראשון יהיה שם האפשרות בסקר, והתא השני יהיה מספר ההצבעות.
  5. get_winner המחזירה את שם האפשרות שקיבלה את מרב ההצבעות.

במקרה של תיקו, החזירו מ־get_winner את אחת האפשרויות המובילות.
החזירו מהפעולות vote, add_option ו־remove_option את הערך True אם הפעולה עבדה כמצופה.
במקרה של הצבעה לאפשרות שאינה קיימת, מחיקת אפשרות שאינה קיימת או הוספת אפשרות שכבר קיימת, החזירו False.

ודאו שהקוד הבא מדפיס רק True עבור התוכנית שכתבתם:


In [ ]:
def cast_multiple_votes(poll, votes):
    for vote in votes:
        poll.vote(vote)


bridge_question = Poll('What is your favourite colour?', ['Blue', 'Yellow'])
cast_multiple_votes(bridge_question, ['Blue', 'Blue', 'Yellow'])
print(bridge_question.get_winner() == 'Blue')
cast_multiple_votes(bridge_question, ['Yellow', 'Yellow'])
print(bridge_question.get_winner() == 'Yellow')
print(bridge_question.get_votes() == [('Yellow', 3), ('Blue', 2)])
bridge_question.remove_option('Yellow')
print(bridge_question.get_winner() == 'Blue')
print(bridge_question.get_votes() == [('Blue', 2)])
bridge_question.add_option('Yellow')
print(bridge_question.get_votes() == [('Blue', 2), ('Yellow', 0)])
print(not bridge_question.add_option('Blue'))
print(bridge_question.get_votes() == [('Blue', 2), ('Yellow', 0)])

משחקי הרעב

קטניס אוורדין הלכה לאיבוד באיזו זירה מעצבנת, ועכשיו היא מחפשת את הסניף הקרוב של אבו־חסן למנה משולשת ראויה.

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

  1. היא בוחרת אקראית באחד מקודקודי הזירה.
  2. היא הולכת מהמקום שבו היא נמצאת את מחצית הדרך עד לקודקוד שבחרה.
  3. היא מסמנת על המפה את הנקודה שהגיעה אליה.

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

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