اگر برنامه نویس پایتون باشین احتمالا تو کدهای مختلف با چیزی به اسم metaclass مواجه شدین و براتون هم سوال شده این چیه و چیکارا میکنه؟ تو این مطلب سعی می کنم تا جایی که می تونم این موضوع رو براتون توضیح بدم و همزمان خودم هم یادبگیرم.
نکته ۱: از اونجایی که دیگه پایتون ۲ توسعه داده نمیشه به خاطر همین تو این مطلب منظور از پایتون، پایتون۳ هستش.
نکته ۲: به طور کلی خیلی وارد عمق ماجرا نمی شیم، اما از اون ور هم خیلی هم سطحی بحث رو جمع نمیکنیم(دوتا مثال بزنیم و تموم بشه بره). بیشتر هدف اینه با موضوع آشنا باشیم تا اگر در آینده به چالشی برخوردیم که متاکلاس می تونست کمک کننده باشه یک ایده ی کلی داشته باشیم.
بزارین قبل از شروع یک نکته کلاسیک که در رابطه با این موضوع وجود داره رو نقل قول کنیم:
Metaclasses are deeper magic than 99% user should ever worry about. If you wonder whether you need them, you don't(the people who actually need them know with certainty that they need them, and don't need an explanation about why).
- Tim Peters (the Python guru who authored the Zen of Python)
سوالی که پیش میاد اینه خب وقتی ۹۹ درصد نباید نگرانش باشیم چرا الان باید نگرانش بشیم؟ جواب ساده ست: کنجکاوی، میل به دانش و فهمیدن. این کنجکاوی نمیزاره آدم راحت بشینه و فیلمش رو ببینه. اون گوشه ذهن داد و هوار راه میندازه و هی میگه که برو یکم درموردش بخون، شاید چیز خوبیه و به دردمون خورد. اصلا شاید اون باگت رو همین موضوع حل میکنه(پیشنهاد وسوسه انگیزیه واقعا)!! البته شاید هم کسی تو جمع
مون باشه که نیاز پیدا کرده درمورد متاکلاس ها اطلاعات بدست بیاره. ما هم یکم درموردش می خونیم شاید کنجکاوی مون راضی شد و اجازه داد ادامه فیلم مون رو ببنیم. البته خدا رو چه دیدی شاید تونستیم باگمون رو هم حل کنیم(واقعیت اینه باگ ها هیچ وقت تموم نمیشن بلکه از باگی به باگی دیگه میریم. و.ف D:)!!
برای شروع با تعریف این موضوع آشنا میشیم. طبق تعریف، metaclass یعنی:
A metaclass is a class whose instances are classes.
تعریف بالا میگه متاکلاس یک کلاسی هستش که کلاس ها نمونه های اون هستند! احتمالا الان با خودتون دارین میگین: یعنی چی؟ چطوری میشه؟ البته حق هم داریم و کاملا طبیعی هستش. اما اینکه بتونیم این تعریف و کلا متاکلاس رو متوجه بشیم، لازمه با کلاس و روند ایجاد اون بیشتر و دقیق تر آشنا بشیم . برای همین اول میریم سراغ اونا و بعد دوباره به این تعریف و توضیح متاکلاس برمی گردیم.
اولین مبحثی هم که به عنوان پیش نیاز درموردش می خونیم، آشنایی با نحوه ایجاد شدن یک کلاس تو پایتون هستش. یعنی وقتی ما یک کلاس رو تعریف می کنیم، پایتون اون رو چطوری می فهمه و می سازه.
اگر بخوایم نحوه ایجاد کلاس رو بدنیم باید با type آشنا بشیم. کلاس type (اگر میخوای بگین type تابعه باید بگم این کلاس به دلیل backward compatibility به این شکل هستش. لینک مستندات کلاس type) به صورت پیشفرض وظیفه ایجاد کلاس ها رو بر عهده داره. یعنی وقتی چیزی مثل تکه کد پایین رو داریم:
class TestCalss: pass
اطلاعات لازم به کلاس type پاس داده میشه تا کلاس رو برامون ایجاد کنه. اما خب سوالی که مطرح میشه اینکه type چه اطلاعاتی رو برای ایجاد کلاس نیاز داره؟ و چه چیزهایی بهش پاس داده میشه؟
کلاس type برای ساخت یک کلاس به سه تا چیز نیاز داره:
یعنی کلاسی که بالا تعریف کردیم این طوری بهش پاس داده میشه:
TestClass = type('TestClass', (object,), {}) # requires a name, bases, and a namespace
حالا اگر بیایم و type کلاس TestCalass رو بگیریم میشه:
>>> type(TestClass) <class 'type'>
اگر هم type یک نمونه(instance) از کلاس TestClass را بگیریم، طبیعتا خواهیم داشت:
>>> test_obj = TestClass() >>> type(test_obj) <class '__main__.TestClass'>
یا اگر بخوایم یک کلاس با جزئیات داشته باشیم، میشه به این صورت عمل کرد:
def test_method(obj): return "Hello World" AnotherTestClass = type( 'AnotherTestClass', # name of the class (TestClass,), # base classes[tuple]. { # attributes 'variable_name1': 100, 'method_name1': lambda x: x*2 'method_name2': test_method } )
چون داریم درمورد کلاس و type حرف میزنیم این تیکه کد رو هم ببنیم بد نیست:
>>> for t in int, float, dict, list, tuple: ... print(type(t)) output: <class 'type'> <class 'type'> <class 'type'> <class 'type'> <class 'type'> >>> print(type(type)) <class 'type'>
یعنی همه چیز در نهایت به type برمیگرده و حتی خود type هم به type برمیگرده.
به صورت شماتیک هم این همچنین چیزی رو خواهیم داشت:
بر اساس چیزهایی گفتیم و کدهایی که دیدیم، همچنین از اون طرف اگر تعریف متاکلاس رو دوباره یادمون بیاریم به این نتیجه میرسیم که، کلاس type همون متاکلاس پیشفرض کلاس های ما هستش چون کلاس هامون نمونه هایی از اون هستند.
خب پس متوجه شدیم که type متاکلاس پیشفرض کلاس ها تو پایتونه، اما آیا میشه metaclass رو شخصی سازی(customize) کرد و متاکلاس خودمون رو بنویسیم؟جواب این سوال بله ست، اما برای نوشتن متاکلاس خودمون باید با روند ساخت یک نمونه از کلاس هم آشنا بشیم به خاطر همین تو بخش بعدی به این موضوع می پردازیم.
موقعی که یک نمونه از کلاس میخواد ساخته بشه تابع __call__ فراخوانی میشه. این تابع هم میاد به ترتیب دو تابع __new__ و __init__ رو فراخوانی میکنه. برای درک بهتر موضوع، این دوتابع رو توضیح مختصری میدیم تا ببنیم به چه کاری میان.
__new__ is the first step of instance creation. It's called first, and is responsible for returning a new instance of your class.
In contrast, __init__ doesn't return anything; it's only responsible for initializing the instance after it's been created.
Use __new__ when you need to control the creation of a new instance.
Use __init__ when you need to control initialization of a new instance.
In general, you shouldn't need to override __new__ unless you're subclassing an immutable type like str, int, unicode or tuple.
احتمالا الان تابع __init__ و اینکه چرا متغیرهامون رو توش تعریف میکنیم منطقی تر به نظر میاد. یک نکته دیگه که وجود داره اینه که __new__ میتونه مواردی مانند __slots__(مطلب درمورد __slots__)، نام کلاس و حتی مواردی مثل کلاس های والد(parent) و... رو تغییر بده چون هنوز نمونه ی کلاس ساخته نشده، ولی __init__ به دلیل فراخوانی بعد از ساخته شدن نمونه این امکان رو نداره. به طور کلی اگر قرار چیزی رو تغییر بدیم بهتره از __new__ استفاده بشه در غیر این صورت منطقی تره که از __init__ استفاده کنیم. اینکه کی کلاس __new__ خود کلاس فراخوانی میشه کی برای والد و ... رو میتونین به کمک مثال هایی که اینجا گفته شده متوجه بشین(حتما یه نگاه بندازین). من بخوام بگم مطلب واقعا طولانی میشه.
پس نتیجه گیری می کنیم که type داره از __new__ استفاده میکنه(به خاطر همین هستش که نمیشه تابع __new__ کلاس type رو override کرد و اگر همچنین کاری بکنیم به ارور میخوریم: TypeError: can't set attributes of built-in/extension type 'type'
).
به عنوان حسن ختام بخش پیش نیازها یکم تو این موضوع عمیق تر بشیم و به صورت خیلی خلاصه ببنیم موقع ساخت نمونه ی کلاس دقیقا چه مراحلی طی میشه:
۱. مشخص شدن متاکلاس مرتبط با کلاس(اینکه type هستش یا ...)
۲. آماده سازی فضای نام(namespace) کلاس(متد __prepare__).
۳. اجرا شدن بدنه کلاس.
۴. در نهایت ساخته شدن نمونه کلاس.
برای جمع بندی این بخش هم دیدن این عکس هم خالی از لطف نیست
اگر هم میخواین درمورد مراحل ساخته شدن نمونه یک کلاس تو پایتون بیشتر بخونین، می تونین از این مطلب کمک بگیرین.
به نظرم دیگه الان دید خوبی نسبت به کلاس، نحوه ساخته شدن و جریان کار پیدا کردیم و می تونیم بریم سراغ metaclass و اینکه چطوری metaclass خودمون رو بنویسیم.
برگردیم به اون تعریفی که اول کار گفته شد و دوباره يادآوریش کنیم. یک نکته که هست اینه که برای متاکلاس، دو تعریف کلاسیک وجود داره و تعریف دوم هم گاها استفاده میشه. برای همین تو این بخش هر دو تعریف رو میاریم:
(1) A metaclass is a class whose instances are classes.
(2) A metaclass is the class of a class.
اما از اونجایی که تعریف اول بیشتر استفاده میشه، شکل کامل این تعریف رو میگیم که اینطوریه:
In object-oriented programming, a metaclass is a class whose instances are classes. Just as an ordinary class defines the behavior of certain objects, a metaclass defines the behavior of certain classes and their instances. Not all object-oriented programming languages support metaclasses. Among those that do, the extent to which metaclasses can override any given aspect of class behavior varies. Metaclasses can be implemented by having classes be first-class citizen, in which case a metaclass is simply an object that constructs classes
(نکته: سازندگان پایتون تعریف دوم رو بیشتر دوست دارن(لینک این موضوع)! و خود آقای Quido van Rossum اینجا(آی پی تون رو به خارج از کشور تغییر بدین تا ۴۰۴ نگیرین!) درمورد متاکلاسِ پایتون توضیحاتی داده که جالب هستش. یک کتاب(Putting Metaclasses to Work) هم معرفی کرده که الهام بخش پیاده سازی کلاس های سبک جدید تو پایتون بوده.)
اما برسیم به تعریف و توضیح بدیم که این تعریف از لحاظ منطقی چطوری ممکنه؟
این تعریف رو میشه در ۴ مرحله توضیح داد:
۱. هر شی یک نمونه از یک کلاس هستش(Every object is an instance of a class)
۲. با توجه به قانون هر چیزی یک شی هستش پس کلاس ها خودشون هم یک شی هستند(everything is an object).
۳. در نتیجه کلاس ها باید نمونه ای از یک کلاس باشند(classes must also be instances of classes).
۴. کلاسی که بقیه کلاس نمونه آن هستند متاکلاس می باشد(A class whose instances are classes is called a metaclass).
الان دیگه منطقی به نظر میاد نه؟
خب کم کم قطعات پازل داره کنار هم قرار میگیره و موضوع برامون شفاف تر میشه.
تا الان
− کلاس و نحوه ساخته شدنش رو فهمیدیم.
− اینکه یک نمونه از کلاس چطوری ساخته میشه رو یاد گرفتیم.
− با تعریف متاکلاس و اینکه به چه معنی هستش هم آشنا شدیم.
خب نوبتی هم که باشه نوبت کد و دیدن ماجرا توی عمله.
بدون هیچ گونه توضیحی بریم سراغ کد و ببنیم که چطوری باید متاکلاس خودمون رو بنویسیم:
class MyMetaClass(type): def __new__(cls, name, bases, attrs): obj = super().__new__(cls, name, bases, attrs) obj.PI = 3.14 return obj
حالا کافیه موقع تعریف کلاس بهش بگیم که از MyMetaClass به عنوان متاکلاس استفاده کنه که برای اونم به این شکل عمل می کنیم:
class TestClass(metaclass=MyMetaClass): pass >>> print(TestClass.PI) 3.14
از اونجایی که ممکنه سر و کارمون به پایتون۲ (هرچند دیگه توسعه نمیشه) بخوره، اشاره کوتاهی بهش میکنیم. توی پایتون ۲ برای مشحض کردن متاکلاس از __metaclass__ استفاده می کنیم. یعنی اینطوری:
class TestClass: __metaclass__ = MyMetaClass
برگردیم به پایتون ۳ و ارث بری، ما برای کلاس TestClass متاکلاس پیشفرض رو عوض کردیم و متاکلاس خودمون رو گذاشتیم. حالا اگر کلاسی از کلاس TestClass ارث بری کنه، از متاکلاس ما استفاده میکنه و درنتیجه متغیر PI رو خواهد داشت، یعنی:
class AnotherTestClass(TestClass): pass >>> print(AnotherTestClass.PI) 3.14
اما ارث بری یک سوالی رو به وجود میاره و اونم اینکه اگر دوتا کلاس والد دوتا متاکلاس متفاوت داشته باشند، چطوری میشه؟
منظور همچنین کدی هستش:
class MetaA(type): pass class MetaB(type): pass class A(metaclass=MetaA): pass class B(metaclass=MetaB): pass class C(A, B): pass
به نظرتون چه اتفاقی میوفته؟ جواب اینکه پایتون اجازه همچین کاری رو نمیده و ارور می گیریم. اروری هم که میگیریم این شکلیه:
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
اما میشه دورش زد! چطوری؟ به این شکل:
class MetaA(type): pass class MetaB(type): pass class MixinMeta(MetaA, MetaB): pass class A(metaclass=MetaA): pass class B(metaclass=MetaB): pass class C(A, B, metaclass=MixinMeta): pass
نکته آخر هم اینکه اگر میخواین درمورد متاکلاس اطلاعات کامل و جامع تری رو بدست بیارین، مطلب پایین رو که به قول بچه های شرکت مطلب خیلی فاخریه رو بخونین. این مطلب اومده اول یک معرفی کلی انجام داده بعد بر اساس کتاب Putting Metaclasses to Work که گفتیم پایتون هم ازش ایده گرفته توضیحات جامعی داده. بعدشم رفته سراغ زبان های دیگه که متاکلاس دارن. مثل java, perl, smalltalk و... و یه توضیحات مختصری هم درباره روش اون ها داده و در نهایت مطلب رو جمع کرده.
درمورد پایتون ۳٫۶ به بعد:
از این نسخه به بعد برای اینکه بشه روند ساخت کلاس رو راحت تر شخصی سازی(customization) کرد، تکنیک هایی ارائه شده که توی PEP-487 معرفی شدن. یکی از این موارد چیزیه به اسم __init_subclass__ که موقع ایجاد کلاس های فرزند فراخوانی میشه. به عنوان مثال:
speakers = {} class Speaker: # `name` is a custom argument def __init_subclass__(cls, name=None): if name is None: name = cls.__name__ speakers[name] = cls class Guido(Speaker): pass class Beazley(Speaker, name='David Beazley'): pass speakers # {'Guido': __main__.Guido, 'David Beazley': __main__.Beazley}`
به نظرم PEP-487 رو بخونین، نکات جالبی رو گفته. مثلا میگه یکی از مزایای این روش کاهش احتمال ارور conflict متاکلاس هستش.
یک اشاره مختصر هم به کاربردهای متاکلاس ها داشته باشیم و بحث رو جمعش کنیم. از جمله کاربردهایی که میشه گفت:
و ... که می تونین سرچ کنین یا خودتون بهش فکر کنین و ببنین چه کارهایی میشه باهاش انجام داد.
امیدوارم که این حس کنجکاوی ما راضی شده باشه و بتونیم ادامه فیلم مون رو ببنیم(حالا از خود این مطلب یک موردی رو پیدا می کنه و میگه برو اونو بخون)!
شاد و خندون باشید :)
منابع:
https://docs.python.org/3.6/glossary.html#term-metaclass
https://docs.python.org/3.6/reference/datamodel.html#metaclasses
http://www.atalon.cz/om/what-is-a-metaclass/
http://python-history.blogspot.com/2013/10/origin-of-metaclasses-in-python.html
https://www.python-course.eu/python3_metaclasses.php
https://realpython.com/python-metaclasses/
https://en.wikipedia.org/wiki/Metaclass
https://stackoverflow.com/questions/674304/why-is-init-always-called-after-new
https://www.geeksforgeeks.org/__new__-in-python/
https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/
https://www.python.org/dev/peps/pep-3115/
https://www.python.org/dev/peps/pep-0487/