מימוש תבנית Singleton בשפת Python

12/09/2018

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

מחר אעביר כאן וובינר על Design Patterns בפייתון ואחת התבניות שאדבר עליה היא Singleton. בוובינר אני רוצה יותר להתמקד במשמעות ובשימושיות של כל תבנית ולכן כאן בפוסט אנסה לחקור יותר לעומק את האפשרויות השונות לבנייתה.

1. הפיתרון הנאיבי באמצעות Class Method

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

class Config:
    _instance = None

    @staticmethod
    def getinstance():
        return Config._instance

Config._instance = Config()

s = Config.getinstance()
s.foo = 10
s.bar = 20

t = Config.getinstance()
print(t.foo)

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

2. פיתרון באמצעות דריסת new

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

בכל מקרה הקוד עם new נראה כך (פייתון 3):

from threading import Lock

class Config:
    _instance = None

    def __new__(cls, *args, **kwargs):        
        if Config._instance is None:
            with Lock():
                if Config._instance is None:
                    Config._instance = super().__new__(cls, *args, **kwargs)

        return Config._instance

s = Config()
s.foo = 10
s.bar = 20
print(s.foo)

t = Config()
print(t.foo)

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

3. פיתרון באמצעות metaclass

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

class MyConfig(Config):
    pass


s = Config()
s.foo = 10
s.bar = 20
print(s.foo)

t = MyConfig()
print(t.foo)

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

הפיתרון השלישי לכן הוא הכללי ביותר אך גם המורכב ביותר להבנה. הרעיון הוא להשתמש במנגנון של Python שנקרא metaclass כדי לדרוס את האופן שבו פייתון יוצר אוביקט חדש בצורה שעוברת בירושה. הפונקציה __init__ של metaclass נקראת בכל פעם שפייתון מזהה מחלקה נוספת בתוכנית ולכן היא תיקרא פעם אחת עבור המחלקה Config ופעם נוספת עבור המחלקה MyConfig. פונקציה זו תאפשר לי לשנות את קוד המחלקה מבחוץ וכך לדרוס את מנגנון יצירת אוביקטים חדשים שלה. הנה הקוד:

class Singleton(type):
    def getinstance(self):
        return self.instance

    def __init__(cls, name, parents, props):
        super().__init__(name, parents, props)
        Singleton.instance = cls()
        cls.__new__ = Singleton.getinstance

class Config(metaclass=Singleton):
    pass

class MyConfig(Config):
    pass


s = Config()
s.foo = 10
s.bar = 20
print(s.foo)

t = MyConfig()
print(t.foo)

מאחר והאיתחול קורה בתוך ה metaclass ולא בעת הגישה הראשונה לאוביקט אפשר לוותר על ההגנה מאתחול כפול. ממילא האיתחול יקרה רק פעם אחת כשפייתון יטען את קוד התוכנית. בנוסף בגלל שהגדרת metaclass עוברת בירושה אין בעיה שמחלקה MyConfig תירש מ Config. בכל מקרה נקבל את אותו אוביקט יחיד.

4. סיכום

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

מחר בעשר בבוקר אעביר וובינר על Python Design Patterns. אם הפוסט הזה דיבר אליכם ואתם רוצים לשמוע עוד או לראות דוגמאות נוספות ל Design Patterns בואו להגיד שלום. נרשמים (בחינם) בקישור כאן:
https://www.tocode.co.il/workshops/47