درس ۱۹: شی گرایی (OOP) در پایتون: متا کلاس (Metaclass) و انتزاع (Abstraction)¶
این درس در ادامه دروس پیش مرتبط با آموزش شی گرایی در پایتون میباشد و به بررسی قابلیت متا کلاس (Metaclass) در پایتون میپردازد و در ادامه یکی دیگر از مفاهیم اصلی برنامهنویسی شی گرا به نام انتزاع (Abstraction) را معرفی و چگونگی پیادهسازی آن در زبان برنامهنویسی پایتون را بررسی خواهیم کرد.
✔ سطح: متوسط
متاکلاس (Metaclass)¶
متاکلاس (Metaclass) [اسناد پایتون] در پایتون یک مفهوم پنهان در رابطه با پیادهسازی برنامهنویسی شی گرا میباشد. اکثر برنامهنویسها چه از آن آگاه باشند و چه خیر، به ندرت از آن استفاده میکنند و شاید شما هیچگاه نیاز به استفاده از این قابلیت نداشته باشید ولی باید بدانید این قابلیتی است که در اکثر زبانهای برنامهنویسی شی گرا ارائه نمیشود!
متاکلاس چیست؟ یک کلاس است و اشیایی که از آن ایجاد میگردند، کلاسها هستند! بارها گفته شد که «هر چیزی در پایتون یک شی است» و این جمله حتی خود موجودیت کلاسها را نیز شامل میشود. با اجرای دستور تعریف کلاس، یک شی از نوع type
در حافظه ایجاد میگردد و از نام کلاس برای اشاره به آن شی استفاده میشود (درس هفدهم). اکنون میتوانیم بگوییم که type
در واقع یک متاکلاس میباشد:
>>> class MyClassName:
... pass
...
>>> obj = MyClassName()
>>> type(obj)
<class '__main__.MyClassName'>
>>> type(MyClassName)
<class 'type'>
>>> isinstance(obj, MyClassName)
True
>>> isinstance(MyClassName, type)
True
در نمونه کد بالا، MyClassName
کلاس یا نوع دادهای بود که ما خودمان آن را تعریف کرده بودیم. با این حال جالب است بدانید تمام انواع پیش تعریف شده در پایتون هم از نوع type
هستند، حتی خود type
!
>>> type(int)
<class 'type'>
>>> type(float)
<class 'type'>
>>> type(dict)
<class 'type'>
>>> type(list)
<class 'type'>
>>> type(tuple)
<class 'type'>
>>> type(str)
<class 'type'>
>>> type(complex)
<class 'type'>
>>> type(bool)
<class 'type'>
>>> type(type)
<class 'type'>
در واقع type
کلاسی است که تمام کلاسهای پایتون به صورت پیشفرض از روی آن ایجاد میگردند. type
یک متا کلاس است و پایتون این قابلیت را به شما میدهد که متا کلاس خودتان را بسازید، بنابراین میتوانید کلاسهایی بسازید که یک شی از متاکلاس شما هستند، هیجان انگیز نیست؟! اکثر زبانهای برنامهنویسی تنها یک پیادهسازی پیشفرض در اختیار برنامهنویس قرار میدهند ولی این قابلیت در پایتون امکانات قدرتمندی برای برنامهنویس فراهم میآورد همچون thread-safety، مدیریت کامل فرآیند ایجاد شی و...
ایجاد یک متاکلاس در پایتون به صورت زیر میباشد:
>>> class SampleMetaClass(type):
... pass
...
>>> class Sample(metaclass=SampleMetaClass):
... pass
...
>>>
>>> type(SampleMetaClass)
<class 'type'>
>>> type(Sample)
<class '__main__.SampleMetaClass'>
برای ایجاد متا کلاس تنها کافی است یک کلاس جدید بسازید که از کلاس type
ارثبری داشته باشد. با ارسال نام این کلاس به پارامتر metaclass
هر کلاس دیگری، میتوان متا کلاس ایجاد شده را به جای متا کلاس پیشفرض (type
) به آن کلاسها انتساب داد. ایجاد متاکلاس کاربردهای جالبی دارد و قطعا قدرت برنامهنویس را در مدیریت فرآیند ایجاد اشیا بیشتر میکند. در ادامه سعی خواهیم کرد با ایجاد یک متا کلاس روند کامل ایجاد یک شی در پایتون را خودمان پیادهسازی کنیم!
بحث اشیای Callable را از درس هفدهم به یاد آورید - هر کلاس در پایتون یک شی از متاکلاس مربوط به خودش میباشد، همچنین گفته شد کلاسها در پایتون Callable هستند، بنابراین هرگاه یک کلاس فراخوانی میشود (در واقع زمانی که یک شی از آن کلاس ایجاد میگردد)، به صورت خودکار متد __call__
متا کلاس آن نیز فراخوانی میگردد.
اکنون زمانی است که میتوانید تمام فرآیند ایجاد یک شی در پایتون را بدانید، آن را در دست بگیرید و هر کاری که نیاز دارید را به انجام برسانید!:
1class MetaClass(type):
2
3 def __call__(self, *args, **kwargs):
4 print('\n------->>> MetaClass __call__')
5 print('self: ', self)
6 print('args: ', args)
7 print('kwargs: ', kwargs)
8
9 obj = self.__new__(self, *args, **kwargs)
10
11 obj.__init__(*args, **kwargs)
12
13 return obj
14
15
16class Sample(metaclass=MetaClass):
17
18 def __new__(cls, *args, **kwargs):
19 print('\n------->>> Sample __new__')
20 print('cls: ', cls)
21 print('args: ', args)
22 print('kwargs: ', kwargs)
23
24 obj = super().__new__(cls)
25 return obj
26
27 def __init__(self, x=0, y=0, z=0):
28 print('\n------->>> Sample __init__')
29 print('self: ', self)
30 print('x: ', x)
31 print('y: ', y)
32 print('z: ', z)
33
34 self.x = x
35 self.y = y
36 self.z = z
37
38
39sample_obj = Sample('p_arg_1', 'p_arg_2', z='k_arg')
------->>> MetaClass __call__
self: <class '__main__.Sample'>
args: ('p_arg_1', 'p_arg_2')
kwargs: {'z': 'k_arg'}
------->>> Sample __new__
cls: <class '__main__.Sample'>
args: ('p_arg_1', 'p_arg_2')
kwargs: {'z': 'k_arg'}
------->>> Sample __init__
self: <__main__.Sample object at 0x7f578772f3d0>
x: p_arg_1
y: p_arg_2
z: k_arg
بر اساس نمونه کد بالا، میتوان فرآیند ایجاد یک شی در پایتون را به ترتیب زیر شرح داد:
کلاس Sample فراخوانی میشود (سطر ۳۹)، در نتیجه به صورت خودکار متد
__call__
کلاسِ کلاس Sample یا همان متا کلاس آن (MetaClass) فراخوانی میشود. توجه داشته باشید که مقدار پارامترself
در این متد برابر با کلاس Sample میباشد (به خروجی سطر پنجم توجه شود -<'class '__main__.Sample>
)، چرا که شی فراخوانی کننده این متد اکنون خود کلاس Sample میباشد.داخل متد
__call__
از متا کلاس، ابتدا متد__new__
تعریف شده داخل کلاس Sample فراخوانی میشود (سطر ۹). توجه داشته باشید که متد__new__
از کلاس Sample نیز متد مرتبط از کلاسobject
(به عنوان superclass) فراخوانی میکند (سطر ۲۴). حاصل یک شی جدید از کلاس Sample خواهد بود که به داخل متد__call__
از متاکلاس برگردانده میشود.داخل متد
__call__
از متا کلاس، اینبار متد__init__
فراخوانی میگردد - پیش از برگرداندن شی Sample ایجاد شده (سطر ۱۱). این متد عملیات initialize یا مقداردهی اولیه را بر روی شی تازه ایجاد شده از کلاس Sample به انجام میرساند.در انتها متد
__call__
از متا کلاس، شی Sample را بازمیگرداند (سطر ۱۳).توجه داشته باشید از آنجا که احتمال ارسال آرگومان به دو صورت «positional arguments» و «keyword arguments» وجود دارد، پارامترها و آرگومانها به گونهای تعریف و ارسال گردیدند که هر دو حالت پوشش داده شود:
args, **kwargs*
با استفاده از مثال پیش، یک کاربرد جالب و مهم از قابلیت تعریف متا کلاس در پایتون را بررسی کردیم، مثالی که شما را با روند ایجاد شی نیز بیشتر آشنا کرد.
در انتهای این بخش جا دارد با امکان تعریف یک متد از نوع Class Method به نام __prepare__
[اسناد پایتون] در پایتون آشنا شویم. به صورت پیشفرض مفسر پایتون پس از اینکه متا کلاسِ یک کلاس را تشخیص میدهد، بلافاصله به دنبال __prepare__
در آن میگردد و چنانچه پیادهسازی شده باشد، آن را فراخوانی و آرگومانهای «متا کلاس»، «نام کلاسی که قرار است یک شی از آن ایجاد گردد»، «یک شی توپِل حاوی فهرست superclassهای آن کلاس - با حفظ ترتیب» و «تعدادی keyword argumentهای احتمالی (آرگومانهای نام=مقدار)» را به آن ارسال میکند [PEP 3115]. خروجی این متد میبایست یک شی دیکشنری (dict
) باشد که در زمان ایجاد و ارزیابی کلاس، مورد استفاده قرار میگیرد. این متد قبل از __call__
فراخوانی میشود و ما میتوانیم از آن برای قرار دادن مقادیری برای استفاده در کلاسهایی که توسط متا کلاس ایجاد میگردند قرار دهیم. برای درک بهتر کاربرد این متد، به نمونه کد زیر توجه نمایید:
1class MetaClass(type):
2
3 @classmethod
4 def __prepare__(metacls, name, bases, **kwargs):
5 print('\n------->>> MetaClass __prepare__')
6 print('metaclass: ', metacls)
7 print('name: ', name)
8 print('superclasses: ', bases)
9 print('extra arguments: ', kwargs)
10
11 return {"class_code": 1633}
12
13
14class Sample(metaclass=MetaClass):
15
16 @classmethod
17 def print_extra_info(cls):
18 print('\n------->>> Sample print_extra_info')
19
20 print ('class_code:', cls.__dict__['class_code'])
21
22
23print ('Sample.__dict__:\n', Sample.__dict__)
24Sample.print_extra_info()
------->>> MetaClass __prepare__
metaclass: <class '__main__.MetaClass'>
name: Sample
superclasses: ()
extra arguments: {}
Sample.__dict__:
{'class_code': 1633, '__module__': '__main__', 'print_extra_info': <classmethod object at 0x7f090a6aa5c0>, '__dict__': <attribute '__dict__' of 'Sample' objects>, '__weakref__': <attribute '__weakref__' of 'Sample' objects>, '__doc__': None}
------->>> Sample print_extra_info
class_code: 1633
دانستن ترتیب مراحل فراخوانی متدهای معرفی شده و همچنین قابلیت پیادهسازی و شخصیسازی آنها میتواند در شرایط خاص خودش برای برنامهنویس بسیار کارگشا باشد.
انتزاع (Abstraction)¶
انتزاع یا تجرید یا Abstraction در شی گرایی فرآیندی است که طی آن تنها ویژگیهای اصلی، آنهم بدون پیادهسازی جزییات ارايه میگردد. در واقع Abstraction برابر است با Implementation hiding. این مفهوم همراه با موجودیت کلاس تعریف میشود، کلاسهای Abstrac. از درس پنجم به یاد داریم که دو نوع کلاس در شیگرایی وجود دارد: ۱- کلاسهای عادی که توانایی نمونهسازی دارند و به آنها ”Concrete Class“ گفته میشود ۲- کلاسهایی که توانایی نمونهسازی ندارند و به آنها ”Abstract Class“ گفته میشود.
«کلاس Abstract» کلاسی است که شامل یک یا چند «متد Abstract» باشد و «متد Abstract» متدی است که اعلان (Declare) شده ولی بدنه آن تعریف (Define) نشده است. کلاسهای Abstract قابلیت نمونهسازی ندارند و نمیتوان از آنها شی ایجاد نمود؛ چرا که هدف از توسعه آنها قرار گرفتن در بالاترین سطح (یا چند سطح بالایی) سلسلهمراتب وراثت، به عنوان کلاس پایه برای ارثبری کلاسهای پایینتر میباشد. در واقع ایده طراحی کلاس Abstract در تعیین یک نقشه توسعه برای subclassها میباشد.
در زبان برنامهنویسی پایتون، Abstraction از طریق ماژول abc
ارايه میشود [اسناد پایتون]. این ماژول دو راه برای ایجاد کلاس Abstract فراهم آورده است:
۱- با استفاده از متا کلاس ABCMeta
[اسناد پایتون]:
1from abc import ABCMeta, abstractmethod
2
3class MyABC(metaclass=ABCMeta):
4
5 @abstractmethod
6 def abs_instance_method(self):
7 """This method should implement how to ....."""
8
9 @classmethod
10 @abstractmethod
11 def abs_class_method(cls):
12 """This method should implement how to ....."""
13
14 @staticmethod
15 @abstractmethod
16 def abs_static_method():
17 """This method should implement how to ....."""
۲- با استفاده از ارثبری کلاس ABC
[اسناد پایتون]، ABC
یک کلاس کمکی است که متا کلاس آن ABCMeta
میباشد و از نسخه 3.4 به پایتون افزوده شده است:
1from abc import ABC, abstractmethod
2
3class MyABC(ABC):
4
5 @abstractmethod
6 def abs_instance_method(self):
7 """This method should implement how to ....."""
8
9 @classmethod
10 @abstractmethod
11 def abs_class_method(cls):
12 """This method should implement how to ....."""
13
14 @staticmethod
15 @abstractmethod
16 def abs_static_method():
17 """This method should implement how to ....."""
توجه
در بحث کلاسهای Abstract زبان برنامهنویسی پایتون میبایست نکات زیر را در نظر داشته باشید:
کلاسهای Abstract میتوانند علاوه بر متدهای Abstract، شامل متدهای معمولی که پیشتر شرح داده شد نیز باشند.
نمیتوان از کلاسهای Abstract شی ایجاد کرد، این نوع کلاس فاقد عملیات نمونهسازی میباشد.
متدهای Abstract با استفاده از دکوراتور
abstractmethod@
از ماژولabc
ایجاد میگردد.متدهای Abstract مانند هر متدی دیگری در پایتون میتوانند از انواع Instance Method یا Class Method یا Static Method باشند. توجه داشته باشید که این قانون در مورد Class Method و Static Method از نسخه 3.3 به بعد پایتون پشتیبانی میگردد.
معمولا بدنه متدهای Abstract بدون پیادهسازی هستند، این کار میتواند با قرار دادن دستورهایی همچون
pass
یاreturn
یاreturn None
نیز انجام شود ولی بهترین کار استفاده از Docstring میباشد (درس ششم)، با یک تیر دو نشان خواهید زد!هیچ اجباری به خالی بودن بدنه (عدم پیادهسازی) متدهای Abstract در داخل کلاس Abstract نیست، این متدها در کلاس Abstract میتوانند شامل یک پیادهسازی پیشفرض باشند، که اشیا subclassها در صورت نیاز میتوانند با استفاده از تابع
()super
(همانطور که پیشتر شرح داده شد)، متد نظیر کلاس Abstract را نیز فراخوانی کنند.کلاسهای Abstract میتوانند مانند دیگر کلاسها در چند سطح از سلسله مراتب وراثت شرکت کنند (از یکدیگر ارثبری داشته باشند)، بنابراین باید توجه داشت که تنها subclassهایی که تمام متدهای Abstract مربوط به superclassهای خود را پیادهسازی کرده باشند به عنوان یک کلاس نرمال یا به اصطلاح Concrete حساب میشود و میتوان از آن نمونهسازی کرد، در غیر این صورت مفسر پایتون آن کلاس را به عنوان یک کلاس Abstract در نظر میگیرد و اجازه نمونهسازی از آن را نخواهد داد.
از کاربرد کلاسهای Abstract میتوان به قرار دادن شرط الزام به پیادهسازی یک سری متد مشخص اشاره کرد. به این صورت که برنامهنویس با تعریف یک کلاس Abstract، میگوید که اشیا مورد نیاز میبایست چه کاری انجام بدهند ولی نمیگوید چگونه، چرا که هر شی میتواند نسبت به نوع یا کلاس خود، یک پیادهسازی متفاوت از انجام یک کار مشترک را داشته باشد. از طرفی تنها این مهم است که تمامی اشیای دریافتی، یک سری متد مورد نیاز را حتما پیادهسازی کرده باشند. اکنون برنامهنویس میتواند با بررسی نوع شی، تنها به اشیایی که نوع آن کلاس Abstract را به ارث بردهاند (مثلا با استفاده از تابع
isinstance
)، اجازه پذیرش برای کار مورد نظر خود را بدهد و از بروز خطا در برنامه جلوگیری کند. در این صورت میتوان مطمئن بود که اشیا از هر کلاسی که ایجاد شده باشند، حتما متدهای مورد نظر ما را پیادهسازی کردهاند.
نمونه کد زیر را در نظر بگیرید:
در برنامه، قرار است کلاس مربوط به دو گونه آبزیان (Aquatics) و پستانداران (Mammals) از حیوانات ایجاد گردد. حیوانات برخی رفتارهای مشترک دارند و برخی رفتارهایی که خاص گونه خودشان میباشد. حتی ممکن است پیادهسازی برخی رفتارها در هر نوع حیوان متناسب با خودش متفاوت باشد. بنابراین ما دو سطح از سلسله مراتب وراثت ایجاد کردهایم و به منظور مرتبط بودن مثال با مبحث جاری، تنها از کلاسهای Abstract برای پیادهسازی supperclassهای مرتبط بهره گرفتیم و پیادهسازی هر رفتار را به خود موجودیت نهایی واگذار کردیم تا کاملا شخصی و نسبت به مورد پیادهسازی گردند. در این مثال تعریف کلاس دو حیوان نهنگ قاتل (Killer Whale) که یک پستاندار دریایی است و خصوصیاتی مشترک از هر دو گروه آبزی و پستاندار را دارد (وراثت چندگانه) و شیر (Lion) که تنها جزو گروه پستانداران میباشد آورده شده است.
1from abc import ABCMeta, abstractmethod
2
3class Animal(metaclass=ABCMeta):
4
5 @abstractmethod
6 def breathing(self):
7 '''Implement breathing skills'''
8
9
10class Aquatic(Animal):
11
12 @abstractmethod
13 def swimming(self):
14 '''Implement swimming skills'''
15
16
17class Mammal(Animal):
18
19 @abstractmethod
20 def breastfeeding(self):
21 '''Implement breastfeeding skills'''
22
23
24
25class KillerWhale(Aquatic, Mammal):
26
27 def breathing(self):
28 print(f'{self.__class__.__name__}: breathing...')
29
30 def swimming(self):
31 print(f'{self.__class__.__name__}: swimming...')
32
33 def breastfeeding(self):
34 print(f'{self.__class__.__name__}: breastfeeding...')
35
36
37class Lion(Mammal):
38
39 def breathing(self):
40 print(f'{self.__class__.__name__}: breathing...')
41
42 def breastfeeding(self):
43 print(f'{self.__class__.__name__}: breastfeeding...')
44
45
46killer_whale = KillerWhale()
47killer_whale.breathing()
48killer_whale.swimming()
49killer_whale.breastfeeding()
50
51print('-' * 30)
52
53lion = Lion()
54lion.breathing()
55lion.breastfeeding()
KillerWhale: breathing...
KillerWhale: swimming...
KillerWhale: breastfeeding...
------------------------------
Lion: breathing...
Lion: breastfeeding...
😊 امیدوارم مفید بوده باشه