برای مشاهده یافته ها از کلید Enter و برای خروج از کلید Esc استفاده کنید.

کلاس های متا(Meta Classes) در پایتون

برای اینکه به خوبی متوجه بشیم که کلاس های متا در پایتون چی هستن و چیکار میکنن، اول از همه لازمه که خیلی خوب مفهوم کلاس رو در پایتون متوجه بشیم.

در بیشتر زبان ها کلاس قطعه کدی است که مشخص میکنه که یک آبجکت چطور ساخته بشه، میتونیم کلاس رو به عنوان نقشه ی داخلی یک آپارتمان شامل اتاق ها، آشپزخانه و … در نظر بگیریم که ممکنه از روی اون نقشه چندین واحد(آبجکت) ساخته بشه که مشابه هم هستند. این موضوع در پایتون هم کاملا درست است. به مثال زیر دقت کنید:

class Sample(): pass my_obj = Sample() print(my_obj)


اما کلاس ها در پایتون یک ویژگی بسیار منحصربفرد دارند و اون هم اینکه کلاس ها هم در پایتون آبجکت هستند.
اجازه بدید یکم بیشتر توضیح بدم، وقتی پایتون به کلمه ی کلیدی class میرسه یک آبجکت در حافظه به اسم اون کلاس میسازه، یعنی وقتی مترجم پایتون به این خط میرسه:

class Sample():

بلافاصله یک آبجکت در حافظه با نام Sample میسازه.
تفاوت این آبجکت با آبجکت های دیگه اینه که، این آبجکت توانایی ساخت آبجکت داره.
اما مثل همه ی آبجکت های دیگه میشه این آبجکت رو کپی کرد، توی متغیر ریخت، اتریبیوت بهش اضافه کرد و پاسش داد به فانکشن. به قطعه کد زیر دقت کنید تا بهتر متوجه بشید:

class Sample(): pass print(Sample) # you can print a class because it's an object def echo(o): print(o) echo(Sample)# you can pass a class as a parameter print(hasattr(Sample, 'new_attribute')) Sample.new_attribute = 'foo' # you can add attributes to a class print(hasattr(Sample, 'new_attribute')) print(Sample.new_attribute) NewSample = Sample# you can assign a class to a variable print(NewSample .new_attribute) print(NewSample ())


حالا که میدونیم کلاس ها آبجکت هستن پس باید راهی باشه تا بتونیم اون ها رو مثل آبجکت های معمولی به صورت داینامیک و در زمان اجرا بسازیم. یک راه ساده میتونه این باشه که کلاس رو در یک فانکشن تعریف کنیم، به کد زیر دقت کنید:

def choose_class(name): if name == 'foo': class Foo(): pass return Foo else: class Bar(): pass return Bar MyClass = choose_class('foo') print(MyClass) # the function returns a class, not an instance print(MyClass()) # you can create an object from this class


ولی خب این راه چندان هم داینامیک نیست و ما مجبوریم که کل کلاس رو بنویسیم.
از اونجایی که کلاس ها آبجکت هستن پس حتما یه چیزی داره اونا رو تولید میکنه، اما اون چیز چیه؟
تابع type رو یادتونه؟ که ازش برای تشخیص تایپ آبجکت ها استفاده میکردیم؟

class Sample(): pass print(type(1)) print(type("1")) print(type(Sample)) print(type(Sample()))

خب حالا که یادتون اومد از type چه استفاده ای میکردیم، باید بگم که این متد یه توانایی فوق العاده هم داره و اون هم ساخت کلاس ها در زمان اجراس. type میتونه توصیف کلاس رو به صورت پارامتر های ورودی بگیره و یک کلاس برگردونه. به signature این متد نگاه کنید:

type(name of the class,
     tuple of the parent class[es] for inheritance (can be empty),
     a dictionary containing attributes names and values)

برای ساخت کلاس Sample که در بالا دیدید، با استفاده از متد type میتونیم اینجوری بنویسیم:

MySampleClass = type('MySampleClass', (), {}) # returns a class object print(MySampleClass) print(MySampleClass())# create an instance with the class


ما در بالا برای اسم کلاس و اسم متغیر که خروجی متد type را نگه میدارد از نام MySampleClass استفاده کردیم که خب این کار الزامی نیست و فقط برای سادگی اینجا انجام شده.
همونطور که در بالا دیدید متد type سومین پارامترش نام ها و مقادیر خصوصیات کلاس است پس

class Foo(object):
    bar = True


را میتوان به این شکل نوشت:

Foo = type('Foo', (), {'bar':True})


و مانند یک کلاس نرمال از آن استفاده کرد:

Foo = type('Foo', (), {'bar':True}) print(Foo) print(Foo.bar) f = Foo() print(f) print(f.bar)


پارامتر دوم متد type هم همونطور که دیدید نام کلاسهاییست که میخواهد از آنها ارث ببرد یعنی:

class FooChild(Foo):
    pass


را میتوان اینگونه نوشت:

Foo = type('Foo', (), {'bar':True}) FooChild = type('FooChild', (Foo,), {}) print(FooChild) print(FooChild.bar) # bar is inherited from Foo


برای افزودن متد به کلاسهایی که با فانکشن type میسازیم خیلی ساده میتونیم متد رو تعریف کنیم و به یک اتریبیوت اون کلاس اساین کنیم:

Foo = type('Foo', (), {'bar':True}) def echo_bar(self): print(self.bar) FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar}) print(hasattr(Foo, 'echo_bar')) print(hasattr(FooChild, 'echo_bar')) my_foo = FooChild() my_foo.echo_bar()


بعد از ساخت کلاس به شکل داینامیک، میتونیم بهش متدهای بیشتری اضافه کنیم، دقیقا مثل کاری که با کلاسهایی که قبلا تعریف میکردیم انجام میدادیم :

Foo = type('Foo', (), {'bar':True}) def echo_bar(self): print(self.bar) FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar}) def echo_bar_more(self): print('yet another method') FooChild.echo_bar_more = echo_bar_more print(hasattr(FooChild, 'echo_bar_more'))


خب چیزایی که تا الآن یاد گرفتیم اینا بود:
کلاس ها در پایتون، آبجکت هستند.
ما میتونیم در زمان اجرا کلاس تعریف کنیم.
پایتون هم دقیقا وقتی کلمه کلیدی class رو میبینیه یک آبجکت از نوع کلاس رو به صورت داینامیک با استفاده از همین فانکشن type و با کمک کلاس های متا میسازه.

کلاس متا(Meta Class) چیست؟

یادتونه که اول همین جلسه گفتیم کلاس ها چیزهایی هستن که ازشون آبجکت میسازیم؟
یعنی میتونیم کلاس رو دستورالعمل ساخت آبجکت فرض کنیم
از طرف دیگه یاد گرفتیم که کلاس ها هم در پایتون آبجکت هستن
حالا دستورالعمل ساخت این آبجکت ها که از نوع کلاس هستند چی هست؟
جواب کلاس متا است
یعنی همونطور که آبجکت از کلاس ساخته میشه، میشه گفت آبجکت های نوع کلاس در پایتون هم از کلاس های متا ساخته میشوند. اگر بخوایم این مفهوم رو با کد تصور کنیم اینجوری میشه:

MyClass = MetaClass()
my_object = MyClass()

دیدیم که با استفاده از فانکشن type میتونیم کلاس تعریف کنیم، مثلا اینجوری:

MyClass = type('MyClass', (), {})


این موضوع به این خاطر است که فانکشن type در واقع یک کلاس متا است، در واقع type کلاس متایی است که پایتون استفاده میکنه تا کلاس ها رو در پشت صحنه بسازه.
احتمالا حالا براتون سواله که اگر type یک متا کلاس هست چرا با حروف کوچک نوشته شده و مثلا اینجوری Type نوشته نشده؟
به نظر من این قضیه به علت همسان شدن این کلاس با کلاس هایی مثل str(کلاسی که از اون میشه آبجکت استرینگ ساخت) یا int(کلاسی که میشه ازش آبجکت integer ساخت) است.
حالا احتمالا مفهوم این جمله رو که شاید بارها شنیده باشیم درک میکنیم که:

همه چیز در پایتون آبجکت است.

اعداد صحیح، رشته ها، فانکشن ها، کلاس ها و …

پس هر چیزی در پایتون حتما از یک کلاس ساخته شده(یادتونه که آبجکت ها رو از کلاس ها میساختیم؟). با استفاده از اتریبیوت __class__ میتونیم چک کنیم که هر آبجکت از چه کلاسی ساخته شده:

age = 35 print("age.__class__: ",age.__class__) name = 'bob' print("name.__class__: ",name.__class__) def foo(): pass print("foo.__class__: ",foo.__class__) class Bar(object): pass b = Bar() print("b.__class__: ", b.__class__) class Foo(Bar): pass f = Foo() print("f.__class__: ",f.__class__)


حالا به نظرتون کلاسی که نهایتا همه ی کلاسها از اون ساخته میشن چی هست؟
بذارید امتحان کنیم:

age = 35 print("age.__class__: ",age.__class__) name = 'bob' print("name.__class__: ",name.__class__) def foo(): pass print("foo.__class__: ",foo.__class__) class Bar(object): pass b = Bar() print("b.__class__: ", b.__class__) class Foo(Bar): pass f = Foo() print("f.__class__: ",f.__class__) print("#########**class of all classes**##########") print("age.__class__.__class__: ",age.__class__.__class__) print("name.__class__.__class__: ", name.__class__.__class__) print("foo.__class__.__class__: ", foo.__class__.__class__) print("b.__class__.__class__: ", b.__class__.__class__) print("f.__class__.__class__:", f.__class__.__class__)


پس همونطور که دیدید کلاس متا چیزی هست که از اون آبجکت های از نوع کلاس ساخته میشه، میشه کلاس متا رو به عنوان نقشه ی ساخت آبجکت های از نوع کلاس تصور کرد.
type هم کلاس متایی هست که پایتون در پشت صحنه ازش استفاده میکنه و خب ما هم خودمون میتونیم کلاس متا بنویسیم.

در پایتون 3 با استفاده از پارامتر metaclass در زمان تعریف کلاس میتونیم مشخص کنیم که آبجکت کلاسمون با استفاده از چه کلاس متایی ساخته بشه:

class Foo(object, metaclass=something):
    ...


پس در واقع هدف اصلی کلاس متا این هست که رفتار عادی پایتون در زمان ساخت کلاس رو تغییر بده، بیاید با هم یه مثال ببینیم.

فرض کنید میخوایم کلاسی داشته باشیم که اتریبیوت های اون با حروف بزرگ نوشته بشه، برای این کار میتونیم اینجوری عمل کنیم:
(راستی یادتون باشه که به پارامتر metaclass هر چیز callable رو میتونیم پاس بدیم و نیازی نیست که حتما یک کلاس بهش بدیم)

# the metaclass will automatically get passed the same argument # that you usually pass to `type` def upper_attr(future_class_name, future_class_parents, future_class_attr): """ Return a class object, with the list of its attribute turned into uppercase. """ # pick up any attribute that doesn't start with '__' and uppercase it uppercase_attr = {} for name, val in future_class_attr.items(): if not name.startswith('__'): uppercase_attr[name.upper()] = val else: uppercase_attr[name] = val # let `type` do the class creation return type(future_class_name, future_class_parents, uppercase_attr) class Foo(metaclass= upper_attr): bar = 'bip' print("hasattr(Foo, 'bar'): ", hasattr(Foo, 'bar')) print("hasattr(Foo, 'BAR'): ", hasattr(Foo, 'BAR')) f = Foo() print("f.BAR: ", f.BAR)


حالا بیاید دقیقا همین مثل بالا رو دوباره بنویسیم فقط اینبار برای کلاس متا واقعا از یک کلاس استفاده کنیم:

class UpperAttrMetaclass(type): def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr): uppercase_attr = {} for name, val in future_class_attr.items(): if not name.startswith('__'): uppercase_attr[name.upper()] = val else: uppercase_attr[name] = val return type(future_class_name, future_class_parents, uppercase_attr) class Foo(metaclass= UpperAttrMetaclass): bar = 'bip' print("hasattr(Foo, 'bar'): ", hasattr(Foo, 'bar')) print("hasattr(Foo, 'BAR'): ", hasattr(Foo, 'BAR')) f = Foo() print("f.BAR: ", f.BAR)

در کد بالا به این نکات دقت کنید:
1- type یک کلاس است پس میشه ازش ارث بری کرد
2- متد __new__ متدی است که قبل از متد __init__ صدا زده میشه، این متد آبجکت رو میسازه و برمیگردونه در حالیکه متد __init__ آبجکی که ساخته شده رو به عنوان پارامتر میگیره و مقدار دهی اولیه(initialize) میکنه. معمولا به ندرت از متد __new__ استفاده میکنیم مگر اینکه بخواهیم ساخت آبجکت رو کنترل کنیم. در این مثال هم دقیقا چون قصد داشتیم ساخت آبجکت رو به دلخواه خودمون تغییر بدیم از این متد استفاده کردیم.

اگر به کد بالا نگاه کنید خیلی این کد OOP نیست چون ما داریم مستقیما type را صدا میزنیم که بهتره این کار رو نکینم و به جاش متد __new__ کلاس پدر رو استفاده کنیم:

class UpperAttrMetaclass(type): def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr): uppercase_attr = {} for name, val in future_class_attr.items(): if not name.startswith('__'): uppercase_attr[name.upper()] = val else: uppercase_attr[name] = val return super().__new__(upperattr_metaclass, future_class_name, future_class_parents, uppercase_attr) class Foo(metaclass= UpperAttrMetaclass): bar = 'bip' print("hasattr(Foo, 'bar'): ", hasattr(Foo, 'bar')) print("hasattr(Foo, 'BAR'): ", hasattr(Foo, 'BAR')) f = Foo() print("f.BAR: ", f.BAR)

در کد بالا حتما دقت کردید که پارامتر upperattr_metaclass رو به متد __new__ پاس دادیم. خیلی نکته خاصی در رابطه با این پارامتر وجد نداره، فقط لازمه بدونید که متد __new__ کلاسی که در اون قرار داره رو به عنوان اولین پارامتر میگیره، دقیقا مثل self که به متدهای معمولی به عنوان اولین پارامتر پاس میدیم و رفرنس به خود آبجکت است.

اگر نیاز داشته باشیم میتونیم به کلاس متا پارامتر هم پاس بدیم، به این شکل:

class Foo(object, metaclass=Thing, kwarg1=value1):
    ...

class Thing(type):
    def __new__(cls, clsname, bases, dct, kwargs1=default):
        ...

پس یک کلاس متا این کارها رو برای ما انجام میده:

  1. جلوگیری از ساخت کلاس
  2. تغییر در کلاس
  3. و برگرداندن کلاس ویرایش شده

حالا ممکنه براتون سوال باشه که چرا باید از کلاس های متا استفاده کنیم، توجهتون رو به یک جمله از یکی از بزرگان پایتون تیم پیترز جلب میکنم:

کلاس های متا خیلی عمیق تر از چیزی هستند که 99 درصد برنامه نویسان پایتون لازم باشه نگرانش باشن. اگر شما نمیدونید که بهش احتیاج دارید یا خیر، مطمئن باشید که احتیاج ندارید. کسانی که بهش احتیاج دارن با قطعیت میدونن و نیازی ندارن که بهشون توضیح بدی که چرا بهش احتیاج دارن

تیم پیترز

با این حال اگر با Django آشنایی داشته باشید یک مثال فوق العاده در Django ORM وجود داره. فرض کنید شما یک مدل به این شکل تعریف کنید:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

اما میتونید اینجوری ازش استفاده کنید:

guy = Person(name='bob', age='35')
print(guy.age)

نکته اینجاست که guy.age یک آبجکت از نوع IntegerField برنمیگردونه و یک int برمیگردونه و مستقیما هم اون رو از دیستابیس میخونه.

نکته آخر

پس دیدیم که همههههههه چیز در پایتون آبجکت است، حتی کلاس ها. دیدیم که کلاس ها آبجکت هایی هستند که میتونن آبجکت بسازن. همه ی این آبجکت ها یا نمونه هایی از کلاس ها هستند و یا نمونه هایی از کلاس های متا و تنها استثنا در دنیای پایتون type هست که خودش کلاس متای خودش هم هست.

کلاس های متا پیچیدگی زیادی دارن و شما میتونید تغییرات کوچک در کلاس ها رو با دو روش دیگه هم در پایتون انجام بدید:

  1. monkey patching
  2. class decorators

در 99 درصد مواقعی که شما قصد دارید در ساخت یک کلاس تغییر ایجاد کنید احتمالا با یکی از دو روش بالا میتونید کارتون رو انجام بدید و نیازی به کلاس های متا نخواهید داشت.