درس ۱۹: شی گرایی (OOP) در پایتون: متا کلاس (Metaclass) و انتزاع (Abstraction)

شی گرایی (OOP) در پایتون: متا کلاس (Metaclass) و انتزاع (Abstraction)

Photo by Giancarlo Revolledo

این درس در ادامه دروس پیش مرتبط با آموزش شی گرایی در پایتون می‌باشد و به بررسی قابلیت متا کلاس (Metaclass) در پایتون می‌پردازد و در ادامه یکی دیگر از مفاهیم اصلی برنامه‌نویسی شی گرا به نام انتزاع (Abstraction) را معرفی و چگونگی پیاده‌سازی آن در زبان برنامه‌نویسی پایتون را بررسی خواهیم کرد.

سطح: متوسط



متاکلاس (Metaclass)

متاکلاس (Metaclass) [اسناد پایتون] در پایتون یک مفهوم پنهان در رابطه با پیاده‌سازی برنامه‌نویسی شی گرا می‌باشد. اکثر برنامه‌نویس‌ها چه از آن آگاه باشند و چه خیر، به ندرت از آن استفاده می‌کنند و شاید شما هیچگاه نیاز به استفاده از این قابلیت نداشته باشید ولی باید بدانید این قابلیتی است که در اکثر زبان‌های برنامه‌نویسی شی گرا ارائه نمی‌شود!

../_images/l19-python-metaclass-type.jpg

متاکلاس چیست؟ یک کلاس است و اشیایی که از آن ایجاد می‌گردند، کلاس‌ها هستند! بارها گفته شد که «هر چیزی در پایتون یک شی است» و این جمله حتی خود موجودیت کلاس‌ها را نیز شامل می‌شود. با اجرای دستور تعریف کلاس، یک شی از نوع 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...


😊 امیدوارم مفید بوده باشه