درس ۲۰: شی گرایی (OOP) در پایتون: Encapsulation و چندریختی (Polymorphism)

شی گرایی (OOP) در پایتون: Encapsulation و چندریختی (Polymorphism)

Photo by sanjiv nayak

این درس در ادامه دروس گذشته مرتبط با آموزش شی گرایی در زبان برنامه‌نویسی پایتون می‌باشد. تاکنون با دو تا از چهار مفهوم مهم در شی‌گرایی آشنا شده‌ایم: وراثت (Inheritance) - درس هجدهم و انتزاع (Abstraction) - درس نوزدهم. این درس به بررسی دو مورد باقی‌مانده، یعنی کپسوله‌سازی (Encapsulation) و چندریختی (Polymorphism) در زبان برنامه‌نویسی پایتون می‌پردازد.

سطح: متوسط



کپسوله‌سازی (Encapsulation)

در مبحث شی گرایی به پنهان‌سازی اطلاعات درونی یک شی و محدود کردن دسترسی به آن‌ها از بیرون، کپسوله‌سازی (Encapsulation) گفته می‌شود - در واقع Encapsulation برابر است با Information hiding.

پیش از هر چیزی لازم است بدانیم که زبان برنامه‌نویسی پایتون از فلسفه‌ای پیروی می‌کند که در جمله «اینجا همه بزرگسال هستیم» "we are all consenting adults here" خلاصه می‌شود! بنابراین این زبان برخلاف زبان‌هایی مانند Java و ++C یک Encapsulation قوی (strong) در اختیار برنامه‌نویس قرار نمی‌دهد. پایتون به برنامه‌نویس اعتماد دارد و می‌گوید «اگر دوست داری در مکان‌های تاریک پرسه بزنی، من مطمئنم که دلیل خوبی داری و هیچ مشکلی ایجاد نمی‌کنی!»

نکته

به صورت پیش‌فرض تمام اجزای داخلی یک کلاس، public هستند و از هر جایی خارج از کلاس مرتبط خود، قابل دستیابی می‌باشند.

نکته

بر اساس یک قرارداد مابین برنامه‌نویسان پایتون،‌ چنانچه ابتدای نام Attributeها و Methodها با یک کاراکتر خط زیرین (_) آغاز شود، این مفهوم را با خود می‌رساند که «دست نزنید مگر داخل همان کلاس یا subclassهای آن». رعایت این قرارداد معادل سطح دسترسی protected در Java و ++C می‌باشد.

نکته

زبان برنامه‌نویسی پایتون از تکنیک «دستکاری نام» یا name mangling [ویکی‌پدیا] پشتیبانی می‌کند. به کمک این تکنیک و با قراردادن دو کاراکتر خط زیرین (__) در ابتدای نام هر یک از اجزای داخلی یک کلاس (Attributeها و Methodها)، می‌توان معادل سطح دسترسی private در Java و ++C را پیاده‌سازی کرد [اسناد پایتون].

به نمونه کد زیر توجه نمایید:

 1class Student:
 2
 3    def __init__(self, name, score=0):
 4        self.name = name
 5        self.__score = score
 6
 7    def display(self):
 8        print('name:', self.name)
 9        print('score:', self.__score)
10
11
12student = Student('Saeid', 70)
13
14#accessing using method
15student.display()
16
17#accessing directly from outside
18print('-' * 10, 'Accessing directly from outside')
19print('name:', student.name)
20print('score:', student.__score)
name: Saeid
score: 70
---------- Accessing directly from outside
name: Saeid
Traceback (most recent call last):
  File "sample.py", line 20, in <module>
    print('score:', student.__score)
AttributeError: 'Student' object has no attribute '__score'

داده‌های private را در خارج از کلاس نمی‌توان مستقیم مورد دستیابی قرار داد و همانطور که از نمونه کد بالا مشاهده می‌شود دستیابی چنین عناصری در پایتون باعث بروز AttributeError می‌شود. اما گفته شد که پایتون Encapsulation قوی ندارد، چه اتفاقی افتاد؟

مفسر پایتون بر اساس تکنیک name mangling، نام تمام عناصری (Attributeها و Methodها) که تنها با دو کاراکتر خط زیرین شروع شده باشند (مانند spam__) را به صورت زیر با افزودن نام کلاس به ابتدای آن تغییر می‌دهد:

_classname__spam

بنابراین اگر برنامه‌نویسی به دنبال دستیابی نام موجود (spam__) در کلاس باشد، چیزی پیدا نخواهد کرد. با این کار پایتون به صورت نسبی امکان دور نگه‌داشتن عناصر را از حالت عمومی فراهم آورده است. برای مثال پیش داریم:

>>> dir(student)
['_Student__score', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'display', 'name']

متدهای Setter و Getter

در برنامه‌نویسی شی گرا چنانچه بخواهیم دسترسی به داده‌ای را به شدت محدود کنیم، به آن داده سطح دسترسی private را اعمال می‌کنیم. اما گاهی می‌خواهیم تنها روند دستیابی و تغییر برخی از داده‌ها را کنترل کنیم - دسترسی مجاز است ولی چگونگی آن مهم است - در این صورت علاوه بر تنظیم سطح دسترسی private به آن عناصر متدهایی را برای تغییر (به عنوان Setter) و دستیابی (به عنوان Getter) آن‌ها نیز می‌بایست ایجاد کنیم:

 1class Student:
 2
 3    def __init__(self, name, score=0):
 4        self.name = name
 5        self.__score = score
 6
 7    def set_score(self, score):
 8        if isinstance(score, int) and  0 <= score <= 100:
 9            self.__score = score
10
11    def get_score(self):
12        return self.__score
13
14
15student = Student('Saeid', 70)
16student.set_score(99)
17student.set_score('100')
18student.set_score(-10)
19print(f'{student.name}, score:', student.get_score())
Saeid, score: 99

چندریختی (Polymorphism)

چندریختی از کلمات یونانی Poly (زیاد) و Morphism (ریخت) گرفته شده است و در برنامه‌نویسی شی گرا به این معنی است که از یک نام یکسان متد برای انواع مختلف می‌توان استفاده کرد.

در مبحث برنامه‌نویسی شی گرا به شیوه‌های زیر می‌توان چندریختی (Polymorphism) را پیاده‌سازی کرد:

  • Method Overriding

  • Method Overloading

  • Operator Overloading

در ادامه به بررسی و پیاده‌سازی هر مورد در زبان برنامه‌نویسی پایتون خواهیم پرداخت.

Method Overriding

این نوع از چندریختی در هنگام پیاده‌سازی وراثت (Inheritance - درس هجدهم) قابل استفاده است و تا کنون نیز بارها از آن بهره گرفتیم!.

در واقع به پیاده‌سازی دوباره یک متد از کلاس superclass در کلاس subclass را Method Overriding می‌گویند. در این مواقع متد superclass در زیر سایه متد هم نام در subclass قرار می‌گیرد و هنگام فراخوانی متد توسط اشیای کلاس subclass، این متد subclass است که فراخوانی می‌گردد:

 1class Animal:
 2
 3    def breathe(self):
 4      print('Animal, breathing...')
 5
 6    def walk(self):
 7      print('Animal, walking...')
 8
 9
10class Dog(Animal):
11
12    def walk(self):
13      print('Dog, walking...')
14
15
16dog = Dog()
17dog.breathe()
18dog.walk()
Animal, breathing...
Dog, walking...

در این نمونه کد، کلاس Dog از کلاس Animal ارث‌بری دارد و متد walk از کلاس Animal را Override کرده است. همانطور که از خروجی مشاهده می‌شود، برخلاف متد breathe، هنگام فراخوانی متد walk توسط شی Dog، متد باز‌پیاده‌سازی شده موجود در این کلاس فراخوانی می‌شود.

نکته

همان‌طور که پیش‌تر نیز انجام می‌دادیم، چنانچه تمایل به فراخوانی متد متناظر در superclass را داشته باشیم، می‌توانیم از تابع super استفاده کنیم.

نکته

اتفاقی که در بحث انتزاع (Abstraction) و ارث‌بری از کلاس‌های Abstract شاهد بودیم نیز در واقع پیروی از همین مبحث بوده و با این تفاوت که Method Overriding اجباری می‌بود.

نکته

در زبان برنامه‌نویسی پایتون تنها این نام متدهاست که در Method Overriding نقش دارد و تعداد پارامترهای تعریف شده در هر متد مهم نمی‌باشد. بنابراین متد همنام موجود در subclass می‌تواند پارامترهای متفاوتی نسبت superclass داشته باشد. البته تغییر در پارامترهای متد باز‌پیاده‌سازی شده چیزی نیست که بخواهیم آن را پیشنهاد بدهیم (به خصوص در بحث پیاده‌سازی متدهای Abstract) چرا که یکی از پیامدهای آن شکسته شدن اصل Liskov Substitution Principle [ویکی‌پدیا] می‌شود.

Method Overloading

این نوع از چندریختی به امکان کنارهم قرار گرفتن چندین متد همنام ولی با پارامترهای متفاوت (از نظر تعداد یا نوع) در کنار هم می‌باشد. یک شی می‌تواند با ارسال آرگومان‌های متفاوت و فراخوانی یک نام یکسان از متد، کارهای متفاوتی را به انجام برساند.

همانطور که در قسمت پیش نیز اشاره شد، در زبان برنامه‌نویسی پایتون تعداد و نوع پارامترهای تعریف شده برای یک تابع یا متد، هیچ ارتباطی با هویت آن متد ندارد و یک متد تنها با نام آن شناسایی می‌شود. بنابراین Method Overloading در پایتون پشتیبانی نمی‌شود و چنانچه چندین متد یا تابع همنام با پارامترهای متفاوت در یک کلاس یا ماژول در کنار هم باشند، خطایی رخ نمی‌دهد ولی باید توجه داشته باشید که متد یا تابع آخر، تمام موارد پیش از خود را در زیر سایه خواهد گرفت:

 1class Animal:
 2
 3    def breathe(self):
 4        print('breathing...')
 5
 6    def walk(self):
 7        print('walking...')
 8
 9    def walk(self, time=30):
10        print(f'{time} minutes, walking...')
11
12    def walk(self, minutes=30, seconds=59):
13        print(f'{minutes} minutes and {seconds} seconds, walking...')
14
15
16animal = Animal()
17animal.walk()
30 minutes and 59 seconds, walking...

همان‌طور که از خروجی نمونه کد بالا مشاهده می‌شود، با فراخوانی متد walk توسط شی Animal، از میان سه متد تعریف شده، این آخرین متد است که اجرا می‌گردد.

Operator Overloading

گونه‌ای از مفهوم چندریختی که در زبان برنامه‌نویسی پایتون پشتیبانی می‌شود، Operator Overloading می‌باشد که به انجام عملیات متفاوت با استفاده یک عملگر (Operator - درس ششم) یکسان اشاره دارد. برای مثال عملگر + هنگامی که به همراه دو شی int قرار بگیرد عمل جمع ریاضی (arithmetic addition) را بین آن دو به انجام می‌رساند ولی زمانی که با دو شی str قرار بگیرد، مقدار آن دو شی رشته را به یکدیگر پیوند می‌دهد (concatenate):

>>> a = 3
>>> b = 'string'

>>> a + a
6
>>> b + b
'stringstring'

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

Binary Operators

Magic Metods

+

__add__(self, other) [اسناد پایتون]

-

__sub__(self, other) [اسناد پایتون]

*

__mul__(self, other) [اسناد پایتون]

/

__truediv__(self, other) [اسناد پایتون]

//

__floordiv__(self, other) [اسناد پایتون]

٪

__mod__(self, other) [اسناد پایتون]

**

__pow__(self, other) [اسناد پایتون]


Comparison Operators

Magic Metods

<

__lt__(self, other) [اسناد پایتون]

>

__gt__(self, other) [اسناد پایتون]

<=

__le__(self, other) [اسناد پایتون]

>=

__ge__(self, other) [اسناد پایتون]

==

__eq__(self, other) [اسناد پایتون]

!=

__ne__(self, other) [اسناد پایتون]



به عنوان نمونه یک کلاس Number جدید می‌سازیم و عملگر + در آن پیاده‌سازی می‌کنیم:

 1class Number:
 2
 3    def __init__(self, number):
 4        self.number = number
 5
 6    # adding two objects
 7    def __add__(self, other_number):
 8        return self.number + other_number.number
 9
10
11a = Number(5)
12b = Number(7)
13
14result = a + b
15
16print(f'{a.number } + {b.number } =', result)
5 + 7 = 12

به عنوان مثالی دیگر، شخصی‌سازی سنجش برابر بودن دو شی:

 1class Student:
 2
 3    def __init__(self, name, score=0):
 4        self.name = name
 5        self.score = score
 6
 7    def __eq__(self, other_student):
 8       return self.score == other_student.score
 9
10
11a = Student('Saeid', 75)
12b = Student('Babak', 75)
13
14print(a == b)
True


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