לדלג לתוכן

7.3 מטה מחלקה (לא חובה) הרצאה

הקדמה

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

מה זה מטה-קלאס? (Metaclass)

  • כמו שלאובייקטים יש type, גם למחלקות יש type, זה נקרא מטה-קלאס.
  • מטה-קלאס זה ה"קלאס" של קלאסים בפייתון, באמצעות מטה-קלאסים אנחנו יכולים לשנות את ההתנהגות של קלאסים.
  • מטה-קלאסים הם כמו קלאסים, אנחנו צריכים קודם כל להגדיר מטה-קלאס ואז נוכל להשתמש במטה-קלאס שלנו כדי לצור קלאסים חדשים.
  • בדיוק כמו קלאסים, גם מטה-קלאסים יכולים לרשת ממטה קלאסים אחרים.
  • כמו שקלאסים יורשים בבסיס מobject, מטה-קלאסים יורשים בבסיס מtype.
    Pasted image 20240203163801.png

הפונקציה המובנית type

  • עד כו השתמשנו בפונקציה המובנית type, כדי לקבל את הtype של אובייקטים שונים, בעצם ניצלנו את הפונקציה הזו לא למטרה האמיתית שלה.
  • כמו שאנחנו יכולים להשתמש במחלקה המובנית object כדי לצור אובייקטים, כמו בדוגמה הזו:
    obj = object()
    
  • אנחנו יכולים להשתמש בפונקציה המובנית type כדי ליצור קלאסים שונים בצורה הבאה:
    type(<class_name>, <base_class>, <members>)
    
    MyClass = type("MyClass", (object,), {"a": 5, "b": lambda x: x+1})
    
  • כמו שהקלאס object שימש אותנו לצור אובייקט, המטה-קלאס type גם יכול לשמש אותנו לצור קלאס.
  • עוד דוגמה:
    def my_init(self, value):
        self.c = value
    
    MyClass = type("MyClass", (object,), {"a": 5, "b": lambda x: x+1, "__init__": my_init})
    obj = MyClass(5)
    obj.c
    
  • יצרנו קלאס שיישם מחדש את המתודה __init__
  • אפשר לחשוב על זה שכאשר אנחנו מגדירים מחלקה בפייתון כמו המחלקה הבאה:
    class MyClass:
        a = 5
        def b(x):
            return x + 1
    
  • הקוד יתורגם ל -
    MyClass = type("MyClass", (object,), {"a": 5, "b": lambda x: x+1})
    

מטה-קלאס משלנו

  • אז איך יוצרים מטה-קלאס משלנו? בדיוק כמו שיוצרים קלאס. אחרי שיצרנו מטה-קלאס אנחנו צריכים להכיל אותה על איזשהו קלאס, כל זה קורה בדרך הבאה:
    class Meta:
        pass
    
    class MyClass(metaclass=Meta):
        pass
    
  • אפשר להשתמש בtype כדי לראות את הסוג של הקלאס,
    type(MyClass)
    

מתודות קסם של מטה-קלאסים

  • חזרה קצרה: מה קורה כאשר אנחנו מיישמים את המתודת קסם __call__ של קלאס?
    class MyClass:
        def __call__(self, *args):
            return "Hello"
    
  • זה יתן לאובייקט את האפשרות להיקרא כמו לפונקציה.
    obj = MyClass()
    obj()  # Output: "Hello"
    
  • מה יקרה אם ניישם את המתודה של __call__ מטה-קלאס?
  • זה יתן לקלאס את האפשרות להיקרא כמו פונקציה.
  • ומתי קלאסים נקראים כמו פונקציות?
  • כשאנחנו יוצרים אובייקטים חדשים.
  • אז המתודה __call__ במטה-קלאס הוא הקוד שנקרא כשיוצרים אובייקטים חדשים מקלאס.
  • כמו שאנחנו מכירים מהפרק הקודם, כאשר אנחנו יוצרים אובייקטים חדשים בפייתון המתודות __init__ ו - __new__ של הקלאס נקראות, אז אפשר להסיק מכך שהקוד של __call__ נראה כך:
    class Meta:
        def __call__(cls, *args, **kwargs):
            obj = cls.__new__(cls, *args, **kwargs)
            if isinstance(obj, cls):
                cls.__init__(obj, *args, **kwargs)
            return obj
    
    class MyClass(metaclass=Meta)
        pass
    
    obj = MyClass()
    
  • שימו לב: __call__ מקבל את הפרמטר cls שמכיל את הקלאס MyClass בדיוק כמו שכשאנחנו מגדירים קלאסים אנחנו מקבלים את האובייקט עם הפרמטר self
  • מטה-קלאסים מגדירים איך קלאסים יוצרים אובייקטים חדשים.

פייתון מאחורי הקלעים

  • מה יקרה כאשר אנחנו נריץ את הקוד הבא?
    class MyMeta(type):
        pass
    class Entity(object):
        pass
    class Animal(object):
        pass
    
    class Dog(Animal, Entity, metaclass=MyMeta, a=1):
        """
        My Dog Class
        """
        name: str
        legs = 4
        def member(self):
            pass
    
  • שימו לב, בקוד אנחנו מגדירים מטהקלאס, מחקות שונות, ומחלקה שיורשת ממחלקות שונות, מקבלת מטה-קלאס שהגדרנו, מקבלת מתודה, דוק-סטרינג, משתנה סטטי עם סוג, משתנה סטטי עם ערך התחלתי, וגם מקבלת כפרמטר a=1.
  • פייתון יתרגם את הקוד למעלה לקוד הבא:
    namespace = MyMeta.__prepare__(name='Dog', bases=(Animal, Entity), kwargs={'a': 1})
    namespace.__setitem__('__module__', __name__)
    namespace.__setitem__('__qualname__', 'Dog')
    namespace.__setitem__('__annotations__', {'name': str})
    namespace.__setitem__('__doc__', 'My Dog Class')
    namespace.__setitem__('legs', 4)
    def member(self):
        pass 
    namespace.__setitem__('member', member)
    
    Dog = MyMeta(name='Dog', bases=(Animal, Entity), namespace=namespace,  kwargs={'a': 1})
    
  • אז מה הקוד עושה?
    • המתודה __prepare__ של מטה-קלאס מקבלת את שם הקלאס, ואיזה קלאסים היא יורשת, וכל מיני פרמטרים. המתודה הזו מחברת את כל הממברים של הקלאסים שממנה היא ירשה כדי לצור קלאס שיורשת את כל הממברים של הקלאסים האלו ומחזירה מילון עם כל הממברים האלו.
    • המתודה __setitem__ של מילון, מגדירה מפתח וערך חדש במילון ופה אנחנו משתמשים בה כדי להוסיף למילון עוד ממברים של הקלאס שמכילים את השם שלה, המתודות שלה, השדות הסטטים שלה, הטייפ הינטינג של השדות הסטטים שלה, והדוק-סטרינג שלה.
    • ובסוף הוא קורא למטהקלאס של הקלאס כדי לצור את הקלאס, הוא מקבל כפרמטר את השם של הקלאס, המחלקות שממנה הקלאס יורש, את כל הממברים של הקלאס, ועוד ארגומנטים כללים.

מטה-קלאס של מטה-קלאס

  • כמו שאנחנו יכולים להגדיר מטה-קלאס לקלאסים, אפשר גם להגדיר מטה-קלאס למטה-קלאס.
  • איך זה בא לידי ביטוי?
    class SuperMeta:
        pass
    
    class Meta(metaclass=SuperMeta):
        pass
    
    class MyClass(metaclass=Meta)
        pass
    
  • כמו מקודם שדברנו על כך שהמתודה __call__ של מטה-קלאס קוראת למתודות __init__ ו - __new__ של הקלאס שאנחנו יוצרים אובייקט חדש, אז מתודה __call__ של המטה-קלאס של המטה-קלאס קוראת למתודות __init__ ו - __new__ של המטה-קלאס שאנחנו יוצרים קלאס חדש.
  • איך זה יראה בקוד?
    class SuperMeta:
        def __call__(metacls, name, bases, namespace, **kwargs):    
            cls = metacls.__new__(metacls, name, bases, namespace, **kwargs)
            if isinstance(cls, metacls):
                metacls.__init__(cls, *args, **kwargs)
            return clspass
    
    class Meta(metaclass=SuperMeta):
        def __new__(cls, name, bases, namespace, **kwargs):
            return super().__new__(cls, name, bases, namespace, **kwargs)
    
        def __init__(cls, *args, **kwargs):
            return super().__init__(cls,  *args, **kwargs)
    
        def __call__(cls, *args, **kwargs):
            obj = cls.__new__(cls, *args, **kwargs)
            if isinstance(obj, cls):
                cls.__init__(obj, *args, **kwargs)
            return obj
    
    
    
    class MyClass(metaclass=Meta)
        def __new__(self, *args, **kwargs):
            return super().__new__(self, *args, **kwargs)
    
        def __init__(*args, **kwargs):
            return super().__init__(self, *args, **kwargs)
    
  • נסכם: כאשר אנחנו יוצרים קלאס חדש, אלו הפעולות שיקרו:
    • זה יריץ את המתודות __prepare__, __new__, ו-__init__ של המטה-קלאס.
    • המתודה __new__ של המטה-קלאס מחזירה קלאס.
    • המתודה __prepare__ מכין את הnamespace הראשוני שהקלאס (המילון)
    • המתודה __init__, מאתחלת ממברים בקלאס עם ערכים.
  • נסכם: כאשר אנחנו יוצרים אובייקט חדש, אלו הפעולות שיקרו:
    • המתודה __call__ של המטה קלאס יקרא ויקרא למתודות __new__ ו - __init__ של הקלאס.
    • המתודה __new__ אמורה לצור אובייקט חדש
    • המתודה __init__ אמורה לאתחל שדות של האובייקט עם ערכים.

פרקטיקה: נכתוב מטה-קלאס משלנו

  • נוכל ליישם מחדש את המתודה __new__ של מטה קלאס כדי לעצב את הממברים השונים שיהיו לכל קלאס מהסוג שלנו.
    class MyMeta:
        def __new__(cls, name, bases, namespace, **kwargs):
            """Construct a class object for a class whose metaclass is Meta."""
            new_cls = super().__new__(cls, name, bases, namespace, **kwargs)
            new_cls.a = 5
            return new_cls
    
    class MyClass(metaclass=MyMeta)
        pass
    
    MyClass.a  # Output: 5
    
  • לכל קלאס מסוג MyMeta אחד הממברים שלה יהיה a עם הערך 5.

יצירה מחדש הדקורטור abstractmethod

  • נשתמש במטה-קלאס כדי לצור מחדש את abstractmethod
    class AbstractMethodError(Exception):
        pass
    
    def abstractmethod(method):
        method.__isabstractmethod__ = True
        return method
    
    class ABCMeta(type):
        def __new__(cls, name, bases, namespace, **kwargs):
            new_cls = super().__new__(cls, name, bases, namespace, **kwargs)
            for name, value in namespace.items():
                if callable(value) and getattr(value, '__isabstractmethod__', False):
                    if not hasattr(new_cls, name):
                        raise AbstractMethodError(f"Abstract method '{name}' must be overridden in subclass '{new_cls.__name__}'.")
            return new_cls
    
    class MyABC(metaclass=ABCMeta):
        pass
    
    class MyClass(MyABC):
        @abstractmethod
        def my_abstract_method(self):
            pass
    
    obj = MyClass()  # Will error.
    
  • קודם כל כתבנו דקורטור שיוצר שדה חדש לפונקציה שקיבל שנקרא __isabstractmethod__,
  • עכשיו כדי לאכוף את השדה הזה, נכתוב מטה קלאס שעובר על כל הממברים שמוגדרים לקלאס (שימו לב ש__new__ מקבל את הקלאס אחרי שהקלאס בן שלה דרס אותה), ובודק האם יש מתודה עם השדה __isabstractmethod__, אם כן הוא מחזיר שגיאה כי זה אומר שלא דרסו את המתודה הזו (לא מימשו אותה מחדש), אם היו ממשים אותה מחדש אז הממבר היה נדרס ו - __isabstractmethod__ לא היה קיים.