Python حرفه‌ای: نکات تکمیلی برنامه‌نویسی شی‌گرا

Professional Python: Additional Notes on Object Oriented Programming

18 اسفند 1399
درسنامه درس 14 از سری پایتون حرفه‌ای
Python حرفه ای: نکات تکمیلی برنامه نویسی شیء‌ گرا (قسمت 14)

نقش متد super در ارث بری

در جلسه قبل با هم کد زیر را نوشتیم:

class User:

    def sign_in(self):

        print('logged in')




    def sign_out(self):

        print('signed out')




    def attack(self):

        return 13







class Wizard(User):

    def __init__(self, name, power):

        self.name = name

        self.power = power




    def attack(self):

        print(User.attack(self))

        print(f'{self.name} just attacked with {self.power} power')







class Archer(User):

    def __init__(self, name, arrows):

        self.name = name

        self.arrows = arrows







user1 = User()

wizard1 = Wizard('Amir', 50)

archer1 = Archer('Ahmad', 13)




print(user1.attack())

wizard1.attack()

print(archer1.attack())

ما در متد attack از کلاس wizard، متد دیگری به نام attack از کلاس user را صدا زده ایم و همانطور که در جلسه قبل مشاهده کردیم کد ما بدون مشکل اجرا می شود. ما می توانیم این مسئله را به روش جالب دیگری نیز توضیح بدهیم. فرض کنید کلاس user یک متد init داشته باشد و خصوصیتی را در آن تعریف کنیم. آیا کلاس های فرزند به این خصوصیت دسترسی خواهند داشت؟ همانطور که در بخش «مهارت یادگیری» در جلسات قبل توضیح دادم بهترین روش یادگیری تست گمان هایمان است بنابراین بهتر است کدهایش را بنویسیم:

class User:

    def __init__(self, email):

        self.email = email




    def sign_in(self):

        print('logged in')




    def sign_out(self):

        print('signed out')







class Wizard(User):

    def __init__(self, name, power):

        self.name = name

        self.power = power




    def attack(self):

        print(f'{self.name} just attacked with {self.power} power')







class Archer(User):

    def __init__(self, name, arrows):

        self.name = name

        self.arrows = arrows







wizard1 = Wizard('Amir', 50)




print(wizard1.email)

من برای کلاس user متد init را تعریف کرده و سپس خصوصیتی به نام email را ساخته ام چرا که کاربران ما برای بازی باید با ایمیل خود وارد شوند. از طرفی برایتان توضیح دادم که در وراثت، تمام کدهای کلاس پدر در دسترس کلاس فرزند نیز خواهد بود بنابراین باید بتوانیم از طریق wizard به خصوصیت email دسترسی داشته باشیم، درست است؟ برای تست این فرضیه، یک بار از کلاس wizard نمونه سازی کرده ام و سپس سعی در دسترسی به email داشته ام. با اجرای کد بالا خطای زیر را دریافت می کنیم:

Traceback (most recent call last):

  File "/mnt/Development/Roxo Academy/Python/test.py", line 30, in <module>

    print(wizard1.email)

AttributeError: 'Wizard' object has no attribute 'email'

چرا؟ در همین بخش توضیح دادم که تمام داده های کلاس پدر در دسترس کلاس فرزند است اما در صورتی که کلاس فرزند این داده ها را overwrite کند، دیگر به داده های کلاس پدر دسترسی نخواهیم داشت. چنین مثالی را در جلسه قبل و در رابطه با متد attack مشاهده کردیم. در این کد نیز همین اتفاق رخ می دهد؛ با اینکه کلاس User متدی به نام __init__ را دارد و درون آن خصوصیتی به نام email را تعریف می کند اما کلاس Wizard نیز عینا همین متد را دارد بنابراین باعث overwrite شدن آن شده و دیگر به متد init کلاس پدر دسترسی نداریم. به نظر شما راه حل چیست؟

یک راه حل ساده اما ناکارآمد این است که email را به صورت پارامتر عادی در متد init کلاس wizard تعریف کنیم و متد init کلاس user را نادیده بگیریم اما این روش دقیقا برخلاف اصول برنامه نویسی شیء گرا است. نه تنها در این روش تکرار کد را داریم، بلکه وراثت را نادیده گرفته ایم. در واقع اگر ایمیل را به صورت یک پارامتر ساده در Wizard دریافت کنیم باید منطق انتساب آن به یک خصوصیت را دوباره بنویسیم که اصلا جالب نیست. روش صحیح انجام این کار به شکل زیر است:

class User:

    def __init__(self, email):

        self.email = email




    def sign_in(self):

        print('logged in')




    def sign_out(self):

        print('signed out')







class Wizard(User):

    def __init__(self, name, power, email):

        User.__init__(self, email)

        self.name = name

        self.power = power




    def attack(self):

        print(f'{self.name} just attacked with {self.power} power')







class Archer(User):

    def __init__(self, name, arrows):

        self.name = name

        self.arrows = arrows







wizard1 = Wizard('Amir', 50, 'email@email.com')




print(wizard1.email)

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

در نسخه های جدید پایتون (ورژن ۳) می توانید به جای نام کلاس، متدی به نام ()super را صدا بزنید:

class User:

    def __init__(self, email):

        self.email = email




    def sign_in(self):

        print('logged in')




    def sign_out(self):

        print('signed out')







class Wizard(User):

    def __init__(self, name, power, email):

        super().__init__(email)

        self.name = name

        self.power = power




    def attack(self):

        print(f'{self.name} just attacked with {self.power} power')







class Archer(User):

    def __init__(self, name, arrows):

        self.name = name

        self.arrows = arrows







wizard1 = Wizard('Amir', 50, 'email@email.com')




print(wizard1.email)

همانطور که می بینید با صدا زدن ()super دیگر نیازی به پاس دادن self نیست بنابراین کد خوانا تر می شود. متد super در هر کلاسی صدا زده شود به کلاس بالاتر یا همان کلاس پدر اشاره می کند بنابراین در wizard به user اشاره خواهد کرد. مزیت دیگر این روش این است که اگر در صورت تغییر نام کلاس پدر، نیازی به تغییر آن در کدهای کلاس فرزند نداریم چرا که به جای نام کلاس پدر از super استفاده کرده ایم.

مفهوم Object Introspection

object introspection یا «درون نگری اشیاء» به قابلیت تشخیص نوع یک شیء در runtime (هنگام اجرای کدها) اشاره می کند. از آنجایی که هر چیزی در پایتون یک شیء است، می توانیم به درون این شیء نگاه کرده (درون نگری) و اطلاعات آن را ببینیم. یکی از متدهای مربوط به introspection متد dir می باشد. متد dir تمام خصوصیات و متدهای درون یک شیء را به ما نشان می دهد:

class User:

    def __init__(self, email):

        self.email = email




    def sign_in(self):

        print('logged in')




    def sign_out(self):

        print('signed out')







class Wizard(User):

    def __init__(self, name, power, email):

        super().__init__(email)

        self.name = name

        self.power = power




    def attack(self):

        print(f'{self.name} just attacked with {self.power} power')







class Archer(User):

    def __init__(self, name, arrows):

        self.name = name

        self.arrows = arrows







wizard1 = Wizard('Amir', 50, 'email@email.com')




print(dir(wizard1))

همانطور که می بینید من متد dir را صدا زده و wizard1 را به آن داده ام. با اجرای این کد نتیجه زیر را می گیریم:

['__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__', 'attack', 'email', 'name', 'power', 'sign_in', 'sign_out']

شاید با خودتان بگویید تعداد این متدها بسیار زیاد است اما نکته ای وجود دارد. در پایتون اشیاء از شیء ای پایه به نام object ارث بری دارند (این اتفاق در پس زمینه می افتد) و طبیعتا به متدهای آن دسترسی دارند. اگر به انتهای نتیجه بالا توجه کنید متوجه خصوصیاتی مانند email و name و sign_out یا دیگر متدهای تعریف شده توسط خودمان می شوید. بسیاری از این متدها با دو علامت آندراسکور (علامت _) شروع می شوند و قبلا به شما توضیح داده بودم که به این دسته از متدها Dunder methods می گوییم. آیا می دانید هدف آن ها چیست؟

متدهای dunder

ما در قسمت های اول این دوره آموزشی با توابع پایتون مانند ()len آشنا شدیم. این توابع با استفاده از متدهای dunder پیاده سازی شده اند. به طور مثال اگر یادتان باشد تابعی به نام str داشتیم که یک رشته ایجاد می کرد و حالا در نتیجه بالا حالت dunder آن را داریم که به صورت __str__ می باشد. به مثال زیر توجه کنید:

class Toy:

    def __init__(self, color, age):

        self.color = color

        self.age = age







new_toy = Toy('red', 2)




print(new_toy.__str__)

print(str(new_toy))

این کد یک کلاس ساده است که یک Toy (اسباب بازی) می سازد و برای این کار رنگ (color) و قدیمی بودن یا سن آن (age) را مشخص می کند. من در این مثال str را یک بار به صورت dunder و یک بار به صورت تابع معمولی صدا زده ام. با اجرای این کد نتیجه زیر را می گیریم:

<__main__.Toy object at 0x7f1c9db03a00>

<__main__.Toy object at 0x7f1c9db03a00>

اگر با دقت به این دو نتیجه نگاه کنید متوجه می شوید که هر دو یک مقدار هستند و به یک مکان واحد در مموری اشاره می کنند (آدرس 0x7f1c9db03a00). اگر یادتان باشد در جلسه قبل به شما گفتم که متدهای dunder را ویرایش و overwrite نکنید. حالا متوجه شدید چرا؟ به مثال زیر توجه کنید:

class Toy:

    def __init__(self, color, age):

        self.color = color

        self.age = age




    def __str__(self):

        return f'{self.color}'







new_toy = Toy('red', 2)




print(new_toy.__str__())

print(str(new_toy))

من در این مثال متد dunder خودمان (str) را overwrite کرده ام. حالا با اجرای این کد نتیجه زیر را می گیریم:

red

red

متوجه شدید؟ علاوه بر متد dunder ما، تابع عادی ()str نیز ویرایش شده است و red را برمی گرداند، البته فقط زمانی که روی شیء ای از کلاس Toy صدا زده شود. برای تست این موضوع می گوییم:

class Toy:

    def __init__(self, color, age):

        self.color = color

        self.age = age




    def __str__(self):

        return f'{self.color}'







new_toy = Toy('red', 2)




print(new_toy.__str__())

print(str(new_toy))

print(str("Another DATA"))

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

red

red

Another DATA

به عبارت ساده تر ___str___ برای کلاس Toy تغییر پیدا کرده است و فقط red را برمی گرداند اما در قسمت های دیگر کد، مثل گذشته عمل می کند و یک رشته می سازد. انجام چنین کاری فقط در برخی از موارد خاص توجیه دارد و غیر از آن باعث بروز رفتارهای ناخواسته در کدهایتان می شود. به همین دلیل است که به صورت قانونی کلی می گوییم متدهای dunder را ویرایش نکنید.

یکی از متدهای جالب dunder متد call می باشد که به ما اجازه صدا زدن یک شیء را می دهد یا به عبارتی مقادیر مختلف را قابل فراخوانی می کند. به مثال زیر توجه کنید:

class Toy:

    def __init__(self, color, age):

        self.color = color

        self.age = age




    def __str__(self):

        return f'{self.color}'




    def __call__(self):

        return f'{self.age}'







new_toy = Toy('red', 2)




print(new_toy())

توجه کنید که new_toy فقط یک شیء است بنابراین در حالت عادی مانند توابع پرانتز نمی گیرد اما از آنجایی که ما متد __call__ را در کلاسش تعریف کرده ایم، می توانیم با قرار دادن پرانتز روبرو این شیء متد __call__ آن را صدا کنیم. با اجرای کد بالا عدد ۲ را دریافت می کنید بنابراین مطمئن می شویم که __call__ صدا زده شده است.

تمرین: کلاسی برای ساخت لیست

از شما می خواهم با استفاده از کلاس ها و برنامه نویسی شیء گرا کلاسی تعریف کنید که لیست بسازد و متد dunder ای به نام __len__ داشته باشد. در مرحله بعدی کاری کنید که __len__ بدون توجه به لیست ساخته شده، عدد ۱۰۰۰ را برگراداند. شاید این تمرین در نگاه اول ساده به نظر بیاید اما نکته خاصی در آن نهفته است؛ شما باید قابلیت لیست ها را حفظ کنید! یعنی تمام متدها و کارهایی که می توان با یک لیست را انجام داد، بتوان با لیست شما نیز انجام داد. تنها تفاوت لیست ساخته شده توسط ما این است که علاوه بر متدهای لیست های عادی، متد __len__ را نیز دارد که عدد ۱۰۰۰ را برمی گرداند. سعی کنید خودتان به این سوال پاسخ بدهید.

class SuperList(list):

    def __len__(self):

        return 1000







super_list = SuperList()




super_list.append(13)

print(super_list)

print(super_list.__len__())

نکته مهمی که در این پاسخ وجود دارد این است که کلاس شما باید از کلاس پیش فرض list ارث بری داشته باشد. ما با مفهوم وراثت آشنا شده ایم و می دانیم با انجام این کار به کدهای کلاس پدر (در اینجا list) دسترسی خواهیم داشت بنابراین راحت ترین کار این است که کلاس خودمان را به عنوان کلاس فرزندِ کلاس list تعریف کنیم! در مرحله بعدی متدی به نام __len__ را تعریف کرده ایم که عدد ۱۰۰۰ را برمی گرداند. حالا خارج از کلاس یک نمونه از آن ساخته ایم. برای مطمئن شدن از اینکه کلاس ما به متدهای لیست ها دسترسی دارد از append استفاده کرده ایم و در نهایت هم نتیجه متد len و هم خود super_list را چاپ کرده ایم. با اجرای کد بالا نتیجه زیر را می گیریم:

[13]

1000

به عبارتی ما یک کلاس ساخته ایم که موارد جدیدی را به قابلیت های لیست ها در زبان پایتون اضافه می کند! در ضمن اگر می خواهید مطمئن شوید که کلاس ما، فرزندِ کلاس List است می توانیم از تابع issubclass استفاده کنیم:

class SuperList(list):

    def __len__(self):

        return 1000







super_list = SuperList()




super_list.append(13)

print(super_list)

print(issubclass(SuperList, list))

برای استفاده از این تابع باید کلاس فرزند را به صورت آرگومان اول و کلاس پدر را به صورت آرگومان دوم پاس بدهید. در صورتی که آرگومان اول فرزند آرگومان دوم باشد مقدار true و در غیر این صورت مقدار false را دریافت می کنید. ما با اجرای کد بالا نتیجه زیر را می گیریم:

[13]

True

بنابراین مطمئن می شویم که SuperList فرزند list است.

ارث بری چند گانه

زبان پایتون از جمله زبان هایی است که به ما اجازه ارث بری چند مرحله ای را می دهد. به کد زیر توجه کنید:

class User:

    def sign_in(self):

        print('logged in')







class Wizard(User):

    def __init__(self, name, power):

        self.name = name

        self.power = power




    def attack(self):

        print(f'{self.name} just attacked with {self.power} power')







class Archer(User):

    def __init__(self, name, arrows):

        self.name = name

        self.arrows = arrows




    def shoot(self):

        self.arrows -= 1

        print(self.arrows)

ما این کد را در جلسه قبل نوشته بودیم اما من کمی آن را تغییر داده ام. همانطور که می بینید یک کلاس پدر (User یا همان کاربر) و دو کلاس فرزند (Wizard و Archer - شخصیت های جادوگر و تیرانداز در بازی) داریم. حالا فرض کنید شخصیت دیگری به نام HybridBorg داریم که شخصیت قوی تر داستان باشد و هم قابلیت های Wizard و هم قابلیت های Archer را داشته باشد. آیا با این حال ما می توانیم از هر دو کلاس ارث بری داشته باشیم؟ بله! به کد زیر توجه کنید:

class User:

    def sign_in(self):

        print('logged in')







class Wizard(User):

    def __init__(self, name, power):

        self.name = name

        self.power = power




    def attack(self):

        print(f'{self.name} just attacked with {self.power} power')







class Archer(User):

    def __init__(self, name, arrows):

        self.name = name

        self.arrows = arrows




    def shoot(self):

        self.arrows -= 1

        return self.arrows







class HybridBorg(Wizard, Archer):

    pass







my_borg = HybridBorg('Borgie', 20)




print(my_borg.attack())

print(my_borg.shoot())

همانطور که می بینید من کلاسی به نام HybridBorg را ساخته ام که از هر دو کلاس Wizard و Archer ارث بری می کند. از طرفی می دانیم که هر دو کلاس Wizard و Archer برای ساخته شدن به دو آرگومان نیاز دارند. آرگومان اول مشترک بوده و name است اما آرگومان دوم در یکی power و در دیگری arrow است (به متد __init__ در این کلاس ها توجه کنید). من متد attack از کلاس wizard و متد shoot از کلاس archer را صدا زده و نتیجه شان را چاپ کرده ام. با اجرای کد بالا نتیجه زیر را می گیریم:

Borgie just attacked with 20 power




Traceback (most recent call last):

  File "/mnt/Development/Roxo Academy/Python/test.py", line 32, in <module>

    my_borg.shoot()

  File "/mnt/Development/Roxo Academy/Python/test.py", line 21, in shoot

    self.arrows -= 1

AttributeError: 'HybridBorg' object has no attribute 'arrows'

به عبارتی متد attack بدون مشکل اجرا شده است اما متد shoot با خطا روبرو شده است و می گوید HybridBorg خصوصیتی به نام arrows ندارد. آیا می دانید چرا؟ اگر به کد بالا دوباره نگاه کنید متوجه می شوید که ما برای کلاس HybridBorg ابتدا از کلاس Wizard ارث بری کرده ایم و Archer به عنوان کلاس دوم آمده است بنابراین متد __init__ آن زودتر اجرا شده و آرگومان دومی که پاس داده بودم (عدد ۲۰) برابر با power خواهد بود نه arrows! برای حل این تضاد بین دو کلاس Archer و Wizard باید کلاس HybridBorg را کمی تغییر بدهیم:

class User:

    def sign_in(self):

        print('logged in')







class Wizard(User):

    def __init__(self, name, power):

        self.name = name

        self.power = power




    def attack(self):

        print(f'{self.name} just attacked with {self.power} power')







class Archer(User):

    def __init__(self, name, arrows):

        self.name = name

        self.arrows = arrows




    def shoot(self):

        self.arrows -= 1

        print(self.arrows)







class HybridBorg(Wizard, Archer):

    def __init__(self, name, power, arrows):

        Wizard.__init__(self, name, power)

        Archer.__init__(self, name, arrows)







my_borg = HybridBorg('Borgie', 20, 50)




my_borg.attack()

my_borg.shoot()

my_borg.sign_in()

همانطور که می بینید من متد __init__ از کلاس های Archer و Wizard را در متد __init__ کلاس خودمان صدا زده ایم تا تمام مقادیر پاس داده شده را به دقت به کلاس مورد نظرشان پاس بدهیم. در نهایت برای تست اینکه تمام این مسائل حل شده است هر سه متد attack و shoot و sign_in را از سه کلاس دیگر صدا زده ایم. با اجرای این کدها نتیجه زیر را می گیریم:

Borgie just attacked with 20 power

49

logged in

بنابراین همه چیز به خوبی کار می کند و مشکل ما حل شده است.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری پایتون حرفه‌ای توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.