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

הפונקציה המובנית type¶
- עד כו השתמשנו בפונקציה המובנית
type, כדי לקבל את הtypeשל אובייקטים שונים, בעצם ניצלנו את הפונקציה הזו לא למטרה האמיתית שלה. - כמו שאנחנו יכולים להשתמש במחלקה המובנית
objectכדי לצור אובייקטים, כמו בדוגמה הזו:
- אנחנו יכולים להשתמש בפונקציה המובנית
typeכדי ליצור קלאסים שונים בצורה הבאה:
- כמו שהקלאס
objectשימש אותנו לצור אובייקט, המטה-קלאסtypeגם יכול לשמש אותנו לצור קלאס. - עוד דוגמה:
- יצרנו קלאס שיישם מחדש את המתודה
__init__ - אפשר לחשוב על זה שכאשר אנחנו מגדירים מחלקה בפייתון כמו המחלקה הבאה:
- הקוד יתורגם ל -
מטה-קלאס משלנו¶
- אז איך יוצרים מטה-קלאס משלנו? בדיוק כמו שיוצרים קלאס. אחרי שיצרנו מטה-קלאס אנחנו צריכים להכיל אותה על איזשהו קלאס, כל זה קורה בדרך הבאה:
- אפשר להשתמש ב
typeכדי לראות את הסוג של הקלאס,
מתודות קסם של מטה-קלאסים¶
- חזרה קצרה: מה קורה כאשר אנחנו מיישמים את המתודת קסם
__call__של קלאס?
- זה יתן לאובייקט את האפשרות להיקרא כמו לפונקציה.
- מה יקרה אם ניישם את המתודה של
__call__מטה-קלאס? - זה יתן לקלאס את האפשרות להיקרא כמו פונקציה.
- ומתי קלאסים נקראים כמו פונקציות?
- כשאנחנו יוצרים אובייקטים חדשים.
- אז המתודה
__call__במטה-קלאס הוא הקוד שנקרא כשיוצרים אובייקטים חדשים מקלאס. - כמו שאנחנו מכירים מהפרק הקודם, כאשר אנחנו יוצרים אובייקטים חדשים בפייתון המתודות
__init__ו -__new__של הקלאס נקראות, אז אפשר להסיק מכך שהקוד של__call__נראה כך:
- שימו לב:
__call__מקבל את הפרמטרclsשמכיל את הקלאסMyClassבדיוק כמו שכשאנחנו מגדירים קלאסים אנחנו מקבלים את האובייקט עם הפרמטרself - מטה-קלאסים מגדירים איך קלאסים יוצרים אובייקטים חדשים.
פייתון מאחורי הקלעים¶
- מה יקרה כאשר אנחנו נריץ את הקוד הבא?
- שימו לב, בקוד אנחנו מגדירים מטהקלאס, מחקות שונות, ומחלקה שיורשת ממחלקות שונות, מקבלת מטה-קלאס שהגדרנו, מקבלת מתודה, דוק-סטרינג, משתנה סטטי עם סוג, משתנה סטטי עם ערך התחלתי, וגם מקבלת כפרמטר 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__של מילון, מגדירה מפתח וערך חדש במילון ופה אנחנו משתמשים בה כדי להוסיף למילון עוד ממברים של הקלאס שמכילים את השם שלה, המתודות שלה, השדות הסטטים שלה, הטייפ הינטינג של השדות הסטטים שלה, והדוק-סטרינג שלה. - ובסוף הוא קורא למטהקלאס של הקלאס כדי לצור את הקלאס, הוא מקבל כפרמטר את השם של הקלאס, המחלקות שממנה הקלאס יורש, את כל הממברים של הקלאס, ועוד ארגומנטים כללים.
- המתודה
מטה-קלאס של מטה-קלאס¶
- כמו שאנחנו יכולים להגדיר מטה-קלאס לקלאסים, אפשר גם להגדיר מטה-קלאס למטה-קלאס.
- איך זה בא לידי ביטוי?
- כמו מקודם שדברנו על כך שהמתודה
__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__של מטה קלאס כדי לעצב את הממברים השונים שיהיו לכל קלאס מהסוג שלנו.
- לכל קלאס מסוג
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__לא היה קיים.