ירושה

הקדמה

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

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

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

אתר השירים

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

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

הדפסת שיר תיראה כך:

"Her Majesty's" / The Beatles ----------------------------- Her Majesty's a pretty nice girl But she doesn't have a lot to say Her Majesty's a pretty nice girl But she changes from day to day I want to tell her that I love her a lot But I gotta get a bellyful of wine Her Majesty's a pretty nice girl Someday I'm going to make her mine, oh yeah Someday I'm going to make her mine ----------------------------- Seen: 1 time(s).

נסו לממש את מחלקת השיר בעצמכם.

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

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


In [ ]:
class Song:
    """Represent a Song in our lyrics site.

    Parameters
    ----------
    name : str
        The name of the song.
    lyrics : str
        The lyrics of the song.
    artists : list of str or str, optional
        Can be either a list, or a string separated by commas.

    Attributes
    ----------
    name : str
        The name of the song.
    lyrics : str
        The lyrics of the song.
    _views : int
        Views counter, which indicates how many times the users printed
        a specific song.
    _artists : list of str
        A list of the song's artists.
    """
    def __init__(self, name, lyrics, artists=None):
        self.name = name
        self.lyrics = lyrics
        self._views = 0
        self._artists = self._reformat_artists(artists)

    def _reformat_artists(self, artists):
        if isinstance(artists, str):
            return self._listify_artists_from_string(artists)
        elif artists is None:
            return []
        return artists

    def _listify_artists_from_string(self, artists):
        """Create list of artists from string."""
        for possible_split_token in (', ', ','):
            if possible_split_token in artists:
                return artists.split(possible_split_token)
        return [artists]

    def add_artist(self, artist):
        """Add an artist to the song's artists list."""
        self._artists.append(artist)

    def remove_artist(self, artist):
        """Remove an artist from the song's artists list."""
        if len(self._artists) <= 1 or artist not in self._artists:
            return False
        self._artists.remove(artist)

    def get_artists(self):
        """Return the song's artists list."""
        return self._artists
    
    def count_words(self):
        """Return the word count in the song's lyrics."""
        return len(self.lyrics.split())

    def __str__(self):
        self._views = self._views + 1
        artists = ', '.join(self.get_artists())
        title = f'"{self.name}" / {artists}'
        separator = "-" * len(title)
        return (
            f"{title}\n"
            + f"{separator}\n"
            + f"{self.lyrics}\n"
            + f"{separator}\n"
            + f"Seen: {self._views} time(s)."
        )


lyrics = """
Her Majesty's a pretty nice girl
But she doesn't have a lot to say
Her Majesty's a pretty nice girl
But she changes from day to day

I want to tell her that I love her a lot
But I gotta get a bellyful of wine
Her Majesty's a pretty nice girl
Someday I'm going to make her mine, oh yeah
Someday I'm going to make her mine
""".strip()
her_majestys = Song("Her Majesty's", lyrics, "The Beatles")
print(her_majestys)
print(her_majestys.count_words())

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

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

הפעולה count_words מפצלת את מילות השיר לרשימה, ומחזירה את מספר האיברים ברשימה.

בקריאה ל־__str__ אנחנו מגדילים את ערכה של התכונה _views ב־1. כך נספור את הפעמים שביקשו להדפיס שיר.

אקרוסטיכון

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

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

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

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

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

הטכניקה

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

התחביר

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


In [ ]:
class Acrostic(Song):
    pass

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


In [ ]:
lyrics = """A boat, beneath a sunny sky
Lingering onward dreamily
In an evening of July -
Children three that nestle near,
Eager eye and willing ear,

Pleased a simple tale to hear -
Long has paled that sunny sky:
Echoes fade and memories die:
Autumn frosts have slain July.
Still she haunts me, phantomwise,
Alice moving under skies
Never seen by waking eyes.
Children yet, the tale to hear,
Eager eye and willing ear,

Lovingly shall nestle near.
In a Wonderland they lie,
Dreaming as the days go by,
Dreaming as the summers die:
Ever drifting down the stream -
Lingering in the golden gleam -
Life, what is it but a dream?"""

In [ ]:
song = Acrostic("A Boat, Beneath a Sunny Sky", lyrics, "Lewis Carroll")
print(song)
print(song.count_words())

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

המחלקה Acrostic יורשת מהמחלקה Song.
פנייה לפעולה או לתכונה של המחלקה Acrostic הריקה, תפנה את הבקשה למחלקת־העל Song שממנה Acrostic יורשת.

כיוון ש־Acrostic ירשה את כל הפעולות ממחלקת־העל שלה, Song, ובהן גם את הפעולה __init__,
יצירת מופע חדש באמצעות קריאה ל־Acrostic קוראת למעשה לפעולה Song.__init__.

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


In [ ]:
help(Acrostic)

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


In [ ]:
class Acrostic(Song):
    def get_acrostic(self):
        song_lines = self.lyrics.splitlines()
        first_chars = (line[0] for line in song_lines if line)
        return ''.join(first_chars)

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


In [ ]:
song = Acrostic("A Boat, Beneath a Sunny Sky", lyrics, "Lewis Carroll")
song.get_acrostic()

שימו לב - אומנם הפעולה קיימת במחלקה Acrostic, אך אין זה אומר שהיא קיימת במחלקה Song:


In [ ]:
song = Song("A Boat, Beneath a Sunny Sky", lyrics, "Lewis Carroll")
song.get_acrostic()

נוכל לראות שפייתון מבינה שמופע שנוצר מ־Acrostic הוא גם Acrostic, אבל הוא גם Song:


In [ ]:
song = Acrostic("A Boat, Beneath a Sunny Sky", lyrics, "Lewis Carroll")
print(isinstance(song, Song))
print(isinstance(song, Acrostic))

אבל לא להפך:


In [ ]:
song = Song("A Boat, Beneath a Sunny Sky", lyrics, "Lewis Carroll")
print(isinstance(song, Song))
print(isinstance(song, Acrostic))

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

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

תרגיל ביניים: ססטי... מה?

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

  • בססטינה יש 39 שורות: שישה בתים של שש שורות כל אחד, ולבסוף בית של שלוש שורות.
  • המילה האחרונה בכל שורה בבית הראשון, מופיעה גם כמילה האחרונה בכל אחת מהשורות מהבתים הבאים אחריה. אם נסמן את שש המילים האחרונות בשש השורות של הבית הראשון באותיות אבגדהו, הרי הסדר שבו יופיעו מילים אלה בבתים הבאים הוא ואהבדג, גודאבה, הגבואד, דהאגוב, בדוהגא.
  • שש מילים אלה מופיעות גם בבית האחרון, שבו שלוש שורות. בשורה הראשונה יופיעו המילים בה, בשורה השנייה – דג, ובשורה השלישית – או.

ממשו את המחלקה Sestina.
המחלקה תכיל את כל התכונות והפעולות של שיר רגיל, נוסף על הפעולה is_valid שתבדוק אם הסטטינה תקינה.
בבדיקתכם, התעלמו מהחוקים הנוגעים לבית האחרון.
הפעולה תחזיר True אם כן ו־False אם לא.
בדקו את הפעולה שלכם על "ססטינת ערפד" של ניל גיימן.

דריסה

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

ניקח כדוגמה את מחלקת Instrumental.
קטע כלי (או שיר אינסטרומנטלי) הוא קטע מוזיקלי ללא שירה.


In [ ]:
class Instrumental(Song):
    pass

ניצור מתוך המחלקה מופע עבור הקטע של Yiruma, יצירתו המהממת River Flows in You:


In [ ]:
song = Instrumental("River Flows in You", "", "Yiruma")
print(song)

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

נדרוס את הפעולה __str__ ונממש צורת תצוגה שמתאימה יותר לקטעים כליים:


In [ ]:
class Instrumental(Song):
    def __str__(self):
        self._views = self._views + 1
        artists = ', '.join(self.get_artists())
        title = f'"{self.name}" / {artists}'
        separator = "-" * len(title)
        return f"{title}\n{separator}\nSeen: {self._views} time(s)."

In [ ]:
song = Instrumental("River Flows in You", "", "Yiruma")
print(song)

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

כמה נחמד!
עכשיו כשנדפיס את המופע, מי שתיקרא כדי להמיר את המופע למחרוזת היא הפעולה Instrumental.__str__ ולא Song.__str__.

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

סדר המחלקות שבו נחפש את הפעולה קיבל את השם "סדר בחירת פעולות" (Method Resolution Order; או MRO).
אפשר לראות את סדר בחירת הפעולות של song אם נקרא ל־Instrumental.mro():


In [ ]:
Instrumental.mro()

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

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


In [ ]:
class Example:
    pass

In [ ]:
e = Example()

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


In [ ]:
print(e)

super

מקום נוסף לשיפור במחלקה Instrumental הוא פעולת ה־__init__.
כיוון שלקטעים כליים אין מילים, הפרמטר השני שאנחנו מעבירים לפעולת האתחול (lyrics) הוא מיותר לחלוטין.


In [ ]:
song = Instrumental("Kiss the rain", "", "Yiruma")
print(song)

היה עדיף לו __init__ של המחלקה Instrumental היה קצת שונה מה־__init__ של המחלקה Song.
מה עושים? את הפעולה שהורשה דורסים!
נגדיר __init__ חדש ללא הפרמטר lyrics:


In [ ]:
class Instrumental(Song):
    def __init__(self, name, artists=None):
        self.name = name
        self.lyrics = ""
        self._views = 0
        self._artists = self._reformat_artists(artists)

    def __str__(self):
        self._views = self._views + 1
        artists = ', '.join(self.get_artists())
        title = f'"{self.name}" / {artists}'
        separator = "-" * len(title)
        return f"{title}\n{separator}\nSeen: {self._views} time(s)."

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

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


In [ ]:
class Instrumental(Song):
    def __init__(self, name, artists=None):
        Song.__init__(self, name=name, lyrics="", artists=artists)

    def __str__(self):
        self._views = self._views + 1
        artists = ', '.join(self.get_artists())
        title = f'"{self.name}" / {artists}'
        separator = "-" * len(title)
        return f"{title}\n{separator}\nSeen: {self._views} time(s)."

מה יקרה כעת בעת יצירת מופע חדש של Instrumental?

  1. הפעולה Instrumental.__init__ תרוץ.
  2. בשורה הראשונה של הפעולה, תיקרא הפעולה Song.__init__ כאשר מועבר לה הפרמטר self באופן מפורש.
  3. כשהפעולה Song.__init__ תרוץ, השורה self.name = name שנמצאת בתוך הפעולה תתייחס למופע שיצרנו ממחלקת Instrumental.
    הפעולה מתנהגת כך כיוון שהעברנו לפרמטר self של הפעולה Song.__init__ את המופע שיצרנו במחלקת Instrumental.

עשינו פה טריק מוזר ואולי קצת מלוכלך.
פנינו ישירות לפעולת הקסם Song.__init__, ו"השתלנו" בה את ה־self שמייצג את המופע הנוכחי.

למה עשינו זאת?
אם נקרא ל־Song(), ייווצר אוטומטית מופע חדש של שיר "רגיל", והוא יהיה זה שייכנס לפרמטר self של Song.__init__.
לעומת זאת, אם נקרא ישירות ל־Song.__init__(), נוכל להעביר את הפרמטר self באופן מפורש, בעצמנו.
הטריק הזה מאפשר לנו להעביר לתוך הפרמטר self של Song.__init__ מופע שיצרנו בעזרת מחלקת Instrumental,
או במילים אחרות – הטריק הזה מאפשר לנו להפעיל את פעולת האתחול של Song עבור המופע שנוצר מ־Instrumental.

למה זה שימושי?
כיוון שאז אנחנו מנצלים את פעולת האתחול של Song שנראית כך:


In [ ]:
def __init__(self, name, lyrics, artists=None):
    self.name = name
    self.lyrics = lyrics
    self._views = 0
    self._artists = self._reformat_artists(artists)

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

זה עובד כיוון שהעברנו את self, המופע שלנו, לפעולה Song.__init__,
מה שיגרור את הגדרתן של self.name, self.lyrics, self._views ו־self._artists עבור המופע שנוצר מ־Instrumental.

קריאה לפעולה שדרסנו במחלקת־העל היא טריק נפוץ ושימושי מאוד.
למעשה, נהוג להשתמש בו הרבה גם על פעולות שהן לא __init__.
נוכל להחיל את אותו הטריק גם על __str__, לדוגמה, ולחסוך את ההעלאה של self._views ב־1:

ממשו בעצמכם את אותו תעלול עבור __str__.
הפתרון מופיע בתא שלהלן.

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


In [ ]:
class Instrumental(Song):
    def __init__(self, name, artists=None):
        Song.__init__(self, name=name, lyrics="", artists=artists)

    def __str__(self):
        Song.__str__(self)
        artists = ', '.join(self.get_artists())
        title = f'"{self.name}" / {artists}'
        separator = "-" * len(title)
        return f"{title}\n{separator}\nSeen: {self._views} time(s)."

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

כיוון שהטריק הזה נפוץ יחסית, פייתון נותנת בידינו דרך נוחה להתייחס למחלקת־העל, כשהפרמטר הראשון שנעביר לפעולה שבה הוא self.
במקום לכתוב בכל פעם את הביטוי המתיש Song.__init__(self), נוכל לכתוב super().__init__().
הפונקציה super מאפשרת לנו להעביר את המופע שיצרנו בתת־המחלקה, לתוך פעולה הנמצאת במחלקת־העל.


In [ ]:
class Instrumental(Song):
    def __init__(self, name, artists=None):
        super().__init__(name=name, lyrics="", artists=artists)

    def __str__(self):
        super().__str__()
        artists = ', '.join(self.get_artists())
        title = f'"{self.name}" / {artists}'
        separator = "-" * len(title)
        return f"{title}\n{separator}\nSeen: {self._views} time(s)."

In [ ]:
song = Instrumental("River Flows in You", "Yiruma")
print(song)
print(song)

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

תרגיל ביניים: דרדסים

כידוע לכם בכפר הדרדסים יש הרבה דרדסים "רגילים", אבל יש גם כמה דרדסים מפורסמים, כמו דרדסבא, דרדסית ודרדשף.

לכל דרדס (Smurf) יש את התכונה name שמכילה את שמו, ואת הפעולות eat ו־sleep.
לדרדס המיוחד "דרדסאבא" (PapaSmurf) יש גם את הפעולה give_order, שמקבלת פעולה של דרדס רגיל ומפעילה אותו עליו.
ל"דרדסית" (Smurfette) יש את הפעולה kill_gargamel, שמקבלת כפרמטר מופע שנוצר ממחלקת Gargamel ומשנה את התכונה is_alive שבו ל־False.
ל"דרדשף" (ChefSmurf) יש את הפעולה create_food, שמקבלת את שם המנה שהוא מכין וכמה "חתיכות" (slices) הוא יצר ממנה.

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

ביקורת על ירושה

בשנות ה־90 העליזות החלה להישמע ביקורת הולכת וגוברת על רעיון הירושה.

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

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

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

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

מחלקת העל השברירית

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

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


In [ ]:
class Clickable:
    def __init__(self):
        self.clicks = 0

    def click(self):
        self.clicks = self.clicks + 1

    def double_click(self):
        self.clicks = self.clicks + 2

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


In [ ]:
class CrazyButton(Clickable):
    def click(self):
        self.double_click()

In [ ]:
buy_now = CrazyButton()
buy_now.double_click()
buy_now.clicks

יום אחד, הסתכל אחיתופל המתכנת על הקוד. "רגע! יש פה קוד כפול!" הוא נזעק,
"הפעולה double_click במחלקה Clickcable עושה כמעט את מה שעושה הפעולה click".
אחיתופל ניגש בחופזה לתקן את הקוד, ולהשתמש ב־click פעמיים במקום בערך המפורש 2:


In [ ]:
class Clickable:
    def __init__(self):
        self.clicks = 0

    def click(self):
        self.clicks = self.clicks + 1

    def double_click(self):
        self.click()
        self.click()


class CrazyButton(Clickable):
    def click(self):
        self.double_click()

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


In [ ]:
buy_now = Clickable()
buy_now.click()
buy_now.double_click()
print(buy_now.clicks)

אך מה יקרה אם נרצה להשתמש ב־CrazyButton?


In [ ]:
buy_now = CrazyButton()
buy_now.click()

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

אז מתי כן ומתי לא?

בספר Code Complete מוצג כלל אצבע שזכה לאהדה בקרב מתכנתים רבים:
כשמתארים קשר בין שתי מחלקות בעזרת המילים "סוג של" (is a), הקשר התכנותי ביניהן יהיה לרוב ירושה.
כשמתארים קשר בין שתי מחלקות בעזרת המילה "יש..." (has a), הקשר התכנותי ביניהן יהיה לרוב הכלה.

אם אנחנו יכולים להגיד "א הוא סוג של ב", כאשר מדובר בשמות של מחלקות, ייתכן שנכון להשתמש בירושה.
לדוגמה: כלב הוא סוג של חיה, ולכן ייתכן שמחלקת Dog תירש ממחלקת Animal.
מכונית היא סוג של רכב, ולכן הגיוני שמחלקת Car תירש ממחלקת Vehicle.

אם אנחנו יכולים להגיד "אצל א יש ב", כאשר מדובר בשמות של מחלקות, ייתכן שנכון להשתמש בהכלה.
לדוגמה: למכונית יש מנוע, ולכן הגיוני שבמחלקה Car תופיע התכונה engine, שהיא מופע שנוצר מהמחלקה Engine.
זה עובד על תכונות באופן כללי, ולא רק על קשר של הכלה. לכפתור באתר יש כמה לחיצות, ולכן הגיוני ש־clicks תהיה תכונה של המחלקה Button.

עקרון ההחלפה של ליסקוב

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

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

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

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


In [ ]:
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def get_area(self):
        return self.width * self.height

    def __str__(self):
        dimensions = f"{self.width}x{self.height}"
        return f"Size of {dimensions} is {self.get_area()}"


class Square(Rectangle):
    def __init__(self, side_size):
        super().__init__(side_size, side_size)

בקוד שלמעלה מימשנו מחלקת מלבן Rectangle, שבה התכונות "גובה" (height) ו"רוחב" (width), ואת הפעולה "חישוב שטח" (get_area).
בקוד ישנה מחלקה נוספת בשם ריבוע (Square) שפעולת האתחול שלה מקבלת רק אורך של צלע אחת, שכן כל צלעותיו של ריבוע זהות.
מחלקת הריבוע יורשת ממחלקת המלבן שבנינו.
הרעיון יעבוד נהדר:


In [ ]:
print(Rectangle(5, 6))
print(Square(3))

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


In [ ]:
print(Rectangle(5, 6))

אמור להתאפשר:


In [ ]:
print(Square(5, 6))

לְמַאי נַפְקָא מִנַּהּ? (יענו, ככה החלטתי לתכנת, למה מה תעשה לי?)

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


In [ ]:
my_square = Square(3)
my_square.width = 5
print(my_square)

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

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


In [ ]:
class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    def set_width(self, width):
        self._width = width

    def set_height(self, height):
        self._height = height

    def get_width(self):
        return self._width

    def get_height(self):
        return self._height

    def get_area(self):
        return self.get_width() * self.get_height()

    def __str__(self):
        dimensions = f"{self.get_width()}x{self.get_height()}"
        return f"Size of {dimensions} is {self.get_area()}"


class Square(Rectangle):
    def __init__(self, side_size):
        super().__init__(side_size, side_size)

    def set_width(self, width):
        super().set_width(width)
        super().set_height(width)

    def set_height(self, height):
        super().set_width(height)
        super().set_height(height)

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

הבעיה הבאה תצוץ כשנבנה פונקציה שאמורה לעבוד על מקרה כללי של מלבן או ריבוע:


In [ ]:
def set_and_print(my_shape):
    my_shape.set_height(2)
    my_shape.set_width(3)
    print(my_shape)


set_and_print(Rectangle(4, 5))
set_and_print(Square(4))

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

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


In [ ]:
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def get_area(self):
        return self.width * self.height

    def __str__(self):
        dimensions = f"{self.width}x{self.height}"
        return f"Size of {dimensions} rectangle is {self.get_area()}"


class Square:
    def __init__(self, side):
        self.side = side
    
    def get_area(self):
        return self.side ** 2

    def __str__(self):
        dimensions = f"{self.side}x{self.side}"
        return f"Size of {dimensions} square is {self.get_area()}"

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

העדיפו הכלה על ירושה כשאפשר

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


In [ ]:
class EmailClient:
    def __init__(self, username, password):
        print("Setting up a new mail client...")
        self._inbox = []
        self.username = username
        self.password = password

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


In [ ]:
class Walla(EmailClient):
    DOMAIN = 'walla.co.il'

    def read(self):
        mail_address = f"{self.username}@{self.DOMAIN}"
        print(f"Reading mail of {mail_address} in Walla: [...]")


class Gmail(EmailClient):
    DOMAIN = 'gmail.com'

    def read(self):
        mail_address = f"{self.username}@{self.DOMAIN}"
        print(f"Reading mail of {mail_address} in Gmail: [...]")

נדגים שימוש במחלקה:


In [ ]:
mail = Walla(username='Yam', password='correcthorsebatterystaple')
mail.read()

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


In [ ]:
mail = Gmail(username='Yam', password='correcthorsebatterystaple')
mail.read()

מה יקרה אם נשתמש בהכלה במקום בירושה?
נתחיל בהתאמת המחלקה EmailClient:


In [ ]:
class EmailClient:
    def __init__(self, username, password, provider):
        print("Setting up a new mail client...")
        self._inbox = []
        self.username = username
        self.password = password
        self.provider = provider
    
    def read(self):
        self.provider.read(self.username)

בקוד שלמעלה הוספנו את התכונה provider ל־EmailClient.
תכונה זו תכיל מופע של ספק הדוא"ל.
המחלקות של שירותי הדוא"ל יישארו כפי שהן, אך לא יירשו מ־EmailClient:


In [ ]:
class Walla:
    DOMAIN = 'walla.co.il'

    def read(self, username):
        mail_address = f"{username}@{self.DOMAIN}"
        print(f"Reading mail of {mail_address} in Walla: [...]")


class Gmail:
    DOMAIN = 'gmail.com'

    def read(self, username):
        mail_address = f"{username}@{self.DOMAIN}"
        print(f"Reading mail of {mail_address} in Gmail: [...]")

נראה דוגמה לשימוש:


In [ ]:
mail = EmailClient(
    username='Yam',
    password='correcthorsebatterystaple',
    provider=Walla(),
)

mail.read()

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


In [ ]:
mail.provider = Gmail()
mail.read()

סיכום

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

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

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

מונחים

ירושה (Inheritance)
מנגנון של שפת התכנות שמאפשר ביסוס של מחלקה א על תכונותיה ופעולותיה של מחלקה ב.
מחלקת־על (Superclass)
המחלקה שממנה מתבצעת הירושה. זו המחלקה שממנה ייגזרו הפעולות והתכונות, ו"יועתקו" למחלקה היורשת.
נקראת גם מחלקת בסיס (base class), מחלקת האם או מחלקת האב (parent class).
תת־מחלקה (Subclass)
המחלקה שמעתיקה את תכונותיה של מחלקה אחרת. זו המחלקה שאליה ייגזרו הפעולות והתכונות של המחלקה שממנה מתבצעת הירושה.
נקראת גם המחלקה הנגזרת (derived class) או מחלקת הבת (child class).
דריסה (Override)
החלפת תכונה או פעולה של מחלקת־העל בתת־מחלקה על ידי הגדרתה מחדש בתת־המחלקה.
מחלקת־העל השברירית (Fragile base class)
תסמונת המתארת כיצד שינוי במחלקת־העל באופן שמסתמן כתקין עבור מחלקת־העל, עלול לשבור את תתי־המחלקות שיורשות ממנה.
עקרון ההחלפה של ליסקוב (Liskov Substitution Principle)
עיקרון שמציע שעבור כל קוד שעושה שימוש במחלקת־על, יהיה אפשר להחליף את ההתייחסות למחלקת־העל בתת־המחלקות היורשות ממנה.
לפי העיקרון, אם ההחלפה אינה אפשרית, יש לשקול מחדש את קשרי הירושה בקוד.
הכלה במקום ירושה (Composition over inheritance)
רעיון תכנותי תיאורטי שמציע לשקול שימוש בהכלה במקום בירושה כשהדבר אפשרי.

קוד לדוגמה

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


In [ ]:
class Product:
    def __init__(self, product_id, name, price):
        self.id = product_id
        self.name = name
        self.price = price

    def __str__(self):
        return f"{self.name} - {self.price}$"

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


In [ ]:
import datetime


class PhoneOrder:
    PHONE_CALL_TOLL_IN_USD = 1.99
    DELIVERY_PRICE_IN_USD = 5
    VAT_IN_PERCENTS = 20
    

    def __init__(self, seller_id, buyer_id, products):
        self.seller = seller_id
        self.buyer = buyer_id
        self.products = products
        self.time = datetime.datetime.now()
        self.price = self.calculate_price()
        self.delivered = False

    def calculate_price(self):
        base_price = sum(product.price for product in self.products)
        return base_price + self._calculate_extra_price(base_price)
    
    def _calculate_extra_price(self, base_price, include_vat=True):
        tax = self.VAT_IN_PERCENTS / 100
        return (
            base_price * tax
            + self.DELIVERY_PRICE_IN_USD
            + self.PHONE_CALL_TOLL_IN_USD
        )

    def __str__(self):
        return (
            f"Buyer #{self.buyer}, created by #{self.seller}.\n"
            + f"Delivered: {self.delivered}.\n"
            + f"{'-' * 40}\n"
            + "\n".join(str(product) for product in self.products)
            + f"\n{'-' * 40}\n"
            + f"Total: {self.price}$"
        )


book1 = Product(1, "The Fountainhead", 27.99)
book2 = Product(2, "Thinking, Fast and Slow", 19.69)
order = PhoneOrder(251, 666, [book1, book2])
print(order)

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

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


In [ ]:
import datetime


class Order:
    DELIVERY_PRICE_IN_USD = 5
    VAT_IN_PERCENTS = 20

    def __init__(self, seller_id, buyer_id, products):
        self.seller = seller_id
        self.buyer = buyer_id
        self.products = products
        self.time = datetime.datetime.now()
        self.price = self.calculate_price()
        self.delivered = False

    def calculate_price(self):
        base_price = sum(product.price for product in self.products)
        return base_price + self._calculate_extra_price(base_price)

    def _calculate_extra_price(
            self, base_price, include_vat=True, include_delivery=True,
    ):
        tax = self.VAT_IN_PERCENTS / 100
        price = base_price * tax
        if include_delivery:
            price = price + self.DELIVERY_PRICE_IN_USD
        return price

    def __str__(self):
        return (
            f"Buyer #{self.buyer}, created by #{self.seller}.\n"
            + f"Delivered: {self.delivered}.\n"
            + f"{'-' * 40}\n"
            + "\n".join(str(product) for product in self.products)
            + f"\n{'-' * 40}\n"
            + f"Total: {self.price}$"
        )


class PhoneOrder(Order):
    PHONE_CALL_TOLL_IN_USD = 1.99

    def _calculate_extra_price(self, base_price, **kwargs):
        base_price = super()._calculate_extra_price(base_price, **kwargs)
        return base_price + self.PHONE_CALL_TOLL_IN_USD


class OnlineOrder(Order):
    pass


class StoreOrder(Order):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.delivered = True

    def _calculate_extra_price(self, base_price, **kwargs):
        return super()._calculate_extra_price(
            base_price, include_delivery=False, **kwargs,
        )

In [ ]:
print("Show all three kinds of orders:\n\n")

book1 = Product(1, "The Fountainhead", 27.99)
book2 = Product(2, "Thinking, Fast and Slow", 19.69)
order = PhoneOrder(seller_id=251, buyer_id=666, products=[book1, book2])
print(order)

print('\n\n')
order = StoreOrder(seller_id=251, buyer_id=666, products=[book1, book2])
print(order)

print('\n\n')
order = OnlineOrder(seller_id=251, buyer_id=666, products=[book1, book2])
print(order)

תרגילים

היררכיה

כתבו תוכנה שמדמה מערכת קבצים.

כל קובץ הוא מסוג מסוים – טקסטואלי, בינארי או תיקייה.
תיקייה היא קובץ שמכיל בתוכו רשימת קבצים, שיכולים להיות טקסטואליים, בינאריים או תיקיות.
נדמיין, לדוגמה, את היררכיית התיקיות הבאה:

week8 │ 1_Inheritance.ipynb │ 2_Inheritance_Part_2.ipynb │ 3_Exceptions.ipynb │ ├───images │ exercise.svg │ logo.jpg │ recall.svg │ └───resources

בדוגמה שלמעלה יש 5 קבצים תחת תיקיית week8: שניים מהם תיקיות (images, resources) ו־3 מהם מחברות.
התיקייה resources ריקה, ובתיקייה images יש את הקבצים הטקסטואליים exercise.svg ו־recall.svg ואת הקובץ הבינארי logo.jpg.
במערכת שלנו, הנתיב לתיקיית week8 הוא /week8, והנתיב לקובץ logo.jpg הוא /week8/images/logo.jpg.

צרו בתוכנה שלכם מערכת לניהול משתמשים. לכל משתמש יש שם משתמש וסיסמה.
משתמש יכול להיות מסוג "מנהל מערכת" או "משתמש רגיל".

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

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

מאפיה

ממשו את משחק המאפיה.

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

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

  1. איש המאפיה יתבקש לרצוח שחקן באמצעות הפעולה kill הממומשת אצל איש המאפיה. השחקן הנרצח יוצא מהמשחק.
  2. השוטר יתבקש לעכב את השחקן שלדעתו הוא איש מאפיה בעזרת הפעולה detain הממומשת אצל השוטר.
  3. כל אחד מהמשתתפים יצביע מי לדעתו הוא איש המאפיה באמצעות הפעולה vote.
    מי שקיבל הכי הרבה קולות בהצבעה מוצא להורג ויוצא מהמשחק. אם יש תיקו – כל השחקנים נשארים במשחק.

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

בונוס: ממשו את השאלה כבוט לטלגרם.