آشنایی با metaclass در پایتون

اگر برنامه نویس پایتون باشین احتمالا تو کدهای مختلف با چیزی به اسم metaclass مواجه شدین و براتون هم سوال شده این چیه و چیکارا میکنه؟ تو این مطلب سعی می کنم تا جایی که می تونم این موضوع رو براتون توضیح بدم و همزمان خودم هم یادبگیرم. https://thewebtier.com/wp-content/uploads/2019/06/Important-Python-Tips-and-Tricks-for-Programmers.jpgنکته ۱: از اونجایی که دیگه پایتون ۲ توسعه داده نمیشه به خاطر همین تو این مطلب منظور از پایتون، پایتون۳ هستش.نکته ۲: به طور کلی خیلی وارد عمق ماجرا نمی شیم، اما از اون ور هم خیلی هم سطحی بحث رو جمع نمیکنیم(دوتا مثال بزنیم و تموم بشه بره). بیشتر هدف اینه با موضوع آشنا باشیم تا اگر در آینده به چالشی برخوردیم که متاکلاس می تونست کمک کننده باشه یک ایده ی کلی داشته باشیم.بزارین قبل از شروع یک نکته کلاسیک که در رابطه با این موضوع وجود داره رو نقل قول کنیم: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:)!!https://media.makeameme.org/created/curiosity-is-not.jpgبرای شروع با تعریف این موضوع آشنا میشیم. طبق تعریف، metaclass یعنی:A metaclass is a class whose instances are classes.تعریف بالا میگه متاکلاس یک کلاسی هستش که کلاس ها نمونه های اون هستند! احتمالا الان با خودتون دارین میگین: یعنی چی؟ چطوری میشه؟ البته حق هم داریم و کاملا طبیعی هستش. اما اینکه بتونیم این تعریف و کلا متاکلاس رو متوجه بشیم، لازمه با کلاس و روند ایجاد اون بیشتر و دقیق تر آشنا بشیم . برای همین اول میریم سراغ اونا و بعد دوباره به این تعریف و توضیح متاکلاس برمی گردیم.اولین مبحثی هم که به عنوان پیش نیاز درموردش می خونیم، آشنایی با نحوه ایجاد شدن یک کلاس تو پایتون هستش. یعنی وقتی ما یک کلاس رو تعریف می کنیم، پایتون اون رو چطوری می فهمه و می سازه.نحوه ایجاد کلاساگر بخوایم نحوه ایجاد کلاس رو بدنیم باید با type آشنا بشیم. کلاس type (اگر میخوای بگین type تابعه باید بگم این کلاس به دلیل backward compatibility به این شکل هستش. لینک مستندات کلاس type) به صورت پیشفرض وظیفه ایجاد کلاس ها رو بر عهده داره. یعنی وقتی چیزی مثل تکه کد پایین رو داریم:class TestCalss: passاطلاعات لازم به کلاس type پاس داده میشه تا کلاس رو برامون ایجاد کنه. اما خب سوالی که مطرح میشه اینکه type چه اطلاعاتی رو برای ایجاد کلاس نیاز داره؟ و چه چیزهایی بهش پاس داده میشه؟کلاس type برای ساخت یک کلاس به سه تا چیز نیاز داره:اسم کلاسکلاس یا کلاس های پایه ای که از اون ها ارث میبره(در قالب tuple).ویژگی های کلاس(attribute) به صورت dict.یعنی کلاسی که بالا تعریف کردیم این طوری بهش پاس داده میشه: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 &quotHello World&quot 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 برمیگرده.به صورت شماتیک هم این همچنین چیزی رو خواهیم داشت:منبع: http://www.atalon.cz/om/what-is-a-metaclassبر اساس چیزهایی گفتیم و کدهایی که دیدیم، همچنین از اون طرف اگر تعریف متاکلاس رو دوباره یادمون بیاریم به این نتیجه میرسیم که، کلاس type همون متاکلاس پیشفرض کلاس های ما هستش چون کلاس هامون نمونه هایی از اون هستند.https://i.pinimg.com/originals/83/d3/ff/83d3ff94ca1e713991d758703c4e8c2e.jpgخب پس متوجه شدیم که 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__).۳. اجرا شدن بدنه کلاس.۴. در نهایت ساخته شدن نمونه کلاس.برای جمع بندی این بخش هم دیدن این عکس هم خالی از لطف نیستhttps://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/class-creation.pngاگر هم میخواین درمورد مراحل ساخته شدن نمونه یک کلاس تو پایتون بیشتر بخونین، می تونین از این مطلب کمک بگیرین.متاکلاس چیست؟به نظرم دیگه الان دید خوبی نسبت به کلاس، نحوه ساخته شدن و جریان کار پیدا کردیم و می تونیم بریم سراغ 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).الان دیگه منطقی به نظر میاد نه؟خب کم کم قطعات پازل داره کنار هم قرار میگیره و موضوع برامون شفاف تر میشه.https://en.dopl3r.com/memes/dank/when-you-connect-the-first-two-pieces-in-a-5000-piece-puzzle-set-oh-yeah-its-all-coming-together/889081 تا الان− کلاس و نحوه ساخته شدنش رو فهمیدیم.− اینکه یک نمونه از کلاس چطوری ساخته میشه رو یاد گرفتیم.− با تعریف متاکلاس و اینکه به چه معنی هستش هم آشنا شدیم.خب نوبتی هم که باشه نوبت کد و دیدن ماجرا توی عمله.ایجاد 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 و... و یه توضیحات مختصری هم درباره روش اون ها داده و در نهایت مطلب رو جمع کرده.لینک مطلب فاخر!https://thumbs.gfycat.com/PointlessEcstaticAmurminnow-small.gifدرمورد پایتون ۳٫۶ به بعد:از این نسخه به بعد برای اینکه بشه روند ساخت کلاس رو راحت تر شخصی سازی(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 متاکلاس هستش.کاربردها:یک اشاره مختصر هم به کاربردهای متاکلاس ها داشته باشیم و بحث رو جمعش کنیم. از جمله کاربردهایی که میشه گفت:استفاده گسترده توی فریم ورک های مختلف. به عنوان مثال django یک نمونه خوبه که می تونین بررسیش کنین(حواستون باشه اون Meta که توی مدل ها و اینا می نویسیم فرق داره با این موضوع و اون برای مدیریت متادیتا هستش).کنترل روند ایجاد نمونه های کلاس(دلیلش هم که مشخصه).امکان اضافه کردن thread-safety پیاده سازی singletonو ... که می تونین سرچ کنین یا خودتون بهش فکر کنین و ببنین چه کارهایی میشه باهاش انجام داد.http://kenscourses.com/tc201winter2016/امیدوارم که این حس کنجکاوی ما راضی شده باشه و بتونیم ادامه فیلم مون رو ببنیم(حالا از خود این مطلب یک موردی رو پیدا می کنه و میگه برو اونو بخون)!شاد و خندون باشید :)منابع:https://docs.python.org/3.6/glossary.html#term-metaclasshttps://docs.python.org/3.6/reference/datamodel.html#metaclasseshttp://www.atalon.cz/om/what-is-a-metaclass/http://python-history.blogspot.com/2013/10/origin-of-metaclasses-in-python.htmlhttps://www.python-course.eu/python3_metaclasses.phphttps://realpython.com/python-metaclasses/https://en.wikipedia.org/wiki/Metaclasshttps://stackoverflow.com/questions/674304/why-is-init-always-called-after-newhttps://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/