منابع ترجمه · Think Python · فصل ۱۷

وراثت (فصل ۱۷ Think Python)

۲۷ دقیقه مطالعه · به‌روزرسانی ۲ آبان ۱۴۰۵

منبع اصلی: Think Python, Chapter 17: Inheritance — Allen B. Downey

💡 این صفحه مثل یک نوت‌بوک Jupyter است: متغیر و تابعی که در یک سلول تعریف شود، در سلول‌های بعدی همین صفحه در دسترس است. سلول‌ها را به ترتیب اجرا کنید، یا با «⏮ تا اینجا» همهٔ سلول‌های قبلیِ اجرانشده را یک‌جا اجرا کنید.

توضیح‌ها:

  • خروجی‌های زیر کدها را با اجرای همین کدها (به همان ترتیب سلول‌های نوت‌بوک) به دست آورده‌ام. ترتیب ورق‌های بُر‌خورده به نسخه‌ی پایتون بستگی دارد (با random.seed(3) که در کتاب آمده)، و آدرس حافظه‌ی شیءها در اجرای شما فرق می‌کند.
  • نمودار شیء کتاب و کد ماژول diagram در این فایل نیامده و به‌جایش توضیح کوتاهی نوشته‌ام.
  • دستور %%add_method_to و %%expect فقط در Jupyter/کتاب کار می‌کنند: اولی متد را به کلاسی که قبلاً تعریف شده اضافه می‌کند و دومی انتظار یک خطای مشخص را اعلام می‌کند. در برنامه‌ی عادی، متدها داخل تعریف کلاس می‌آیند.
  • برای تمرین‌ها، خروجی‌های آزمون را با راه‌حل‌هایی که خودم نوشتم و اجرا کردم آورده‌ام؛ جایی که کتاب خودش نوشته «should return True» خروجی را تکرار نکرده‌ام.

ویژگی زبانی‌ای که بیش از همه با برنامه‌نویسی شیءگرا همراه می‌شود وراثت (inheritance) است. وراثت توانایی تعریف یک کلاس جدید است که نسخه‌ی تغییریافته‌ی یک کلاس موجود باشد. در این فصل وراثت را با کلاس‌هایی نشان می‌دهم که ورق‌های بازی، دسته‌های ورق و دست‌های پوکر را نمایش می‌دهند. اگر پوکر بازی نمی‌کنید نگران نباشید – آنچه لازم است بدانید را می‌گویم.

۱۷.۱. نمایش ورق‌ها

در یک دسته‌ی ورق استاندارد ۵۲ ورق وجود دارد – هر کدام به یکی از چهار خال (suit) و یکی از سیزده رتبه (rank) تعلق دارد. خال‌ها پیک (Spades)، دل (Hearts)، خشت (Diamonds) و گشنیز (Clubs) هستند. رتبه‌ها آس (Ace)، ۲، ۳، ۴، ۵، ۶، ۷، ۸، ۹، ۱۰، سرباز (Jack)، بی‌بی (Queen) و شاه (King) هستند. بسته به بازی‌ای که می‌کنید، آس می‌تواند بالاتر از شاه یا پایین‌تر از ۲ باشد.

اگر بخواهیم شیء جدیدی برای نمایش یک ورق تعریف کنیم، روشن است که صفت‌ها باید چه باشند: rank و suit. اینکه صفت‌ها از چه نوعی باشند کمتر روشن است. یک امکان استفاده از رشته‌هایی مثل 'Spade' برای خال‌ها و 'Queen' برای رتبه‌هاست. مشکل این پیاده‌سازی این است که مقایسه‌ی ورق‌ها برای اینکه ببینیم کدام رتبه یا خال بالاتری دارد آسان نخواهد بود.

یک راه جایگزین استفاده از اعداد صحیح برای کدگذاری (encode) رتبه‌ها و خال‌هاست. در اینجا «کدگذاری» یعنی نگاشتی بین عددها و خال‌ها یا بین عددها و رتبه‌ها تعریف می‌کنیم. این نوع کدگذاری قرار نیست مخفی باشد (آن «رمزنگاری» (encryption) می‌شد).

برای مثال، این جدول خال‌ها و کدهای صحیح متناظرشان را نشان می‌دهد:

خال کد
Spades (پیک) 3
Hearts (دل) 2
Diamonds (خشت) 1
Clubs (گشنیز) 0

با این کدگذاری، می‌توانیم خال‌ها را با مقایسه‌ی کدهایشان مقایسه کنیم.

برای کدگذاری رتبه‌ها، از عدد صحیح 2 برای رتبه‌ی 2، 3 برای 3 و همین‌طور تا 10 استفاده می‌کنیم. جدول زیر کدهای ورق‌های تصویری را نشان می‌دهد.

رتبه کد
Jack (سرباز) 11
Queen (بی‌بی) 12
King (شاه) 13

و می‌توانیم برای نمایش آس هم 1 و هم 14 به کار ببریم، بسته به اینکه بخواهیم پایین‌تر یا بالاتر از بقیه‌ی رتبه‌ها در نظر گرفته شود.

برای نمایش این کدگذاری‌ها، از دو فهرست رشته استفاده می‌کنیم، یکی با نام خال‌ها و دیگری با نام رتبه‌ها.

این تعریف کلاسی است که یک ورق بازی را نشان می‌دهد، با این فهرست‌های رشته به‌صورت متغیرهای کلاس (class variables)، یعنی متغیرهایی که داخل تعریف کلاس تعریف می‌شوند، اما نه داخل یک متد.

class Card:
    """Represents a standard playing card."""

    suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
    rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7', 
                  '8', '9', '10', 'Jack', 'Queen', 'King', 'Ace']

اولین عنصر rank_names برابر None است چون ورقی با رتبه‌ی صفر وجود ندارد. با گنجاندن None به‌عنوان نگه‌دارنده‌ی جا، فهرستی با این ویژگی خوب می‌گیریم که اندیس 2 به رشته‌ی '2' نگاشت می‌شود و همین‌طور الی آخر.

متغیرهای کلاس به کلاس مربوطند، نه به یک نمونه از کلاس، پس می‌توانیم این‌طور به آن‌ها دسترسی داشته باشیم.

Card.suit_names
['Clubs', 'Diamonds', 'Hearts', 'Spades']

می‌توانیم از suit_names برای جست‌وجوی یک خال و گرفتن رشته‌ی متناظر استفاده کنیم.

Card.suit_names[0]
'Clubs'

و از rank_names برای جست‌وجوی یک رتبه.

Card.rank_names[11]
'Jack'

۱۷.۲. صفت‌های Card

این یک متد __init__ برای کلاس Card است – suit و rank را به‌عنوان پارامتر می‌گیرد و به صفت‌هایی با همان نام‌ها نسبت می‌دهد.

%%add_method_to Card

    def __init__(self, suit, rank):
        self.suit = suit
        self.rank = rank

حالا می‌توانیم یک شیء Card را این‌طور بسازیم.

queen = Card(1, 12)

می‌توانیم از نمونه‌ی جدید برای دسترسی به صفت‌ها استفاده کنیم.

queen.suit, queen.rank
(1, 12)

استفاده از نمونه برای دسترسی به متغیرهای کلاس هم مجاز است.

queen.suit_names
['Clubs', 'Diamonds', 'Hearts', 'Spades']

اما اگر از کلاس استفاده کنید، روشن‌تر است که آن‌ها متغیر کلاس‌اند، نه صفت.

۱۷.۳. چاپ ورق‌ها

این یک متد __str__ برای شیءهای Card است.

%%add_method_to Card

    def __str__(self):
        rank_name = Card.rank_names[self.rank]
        suit_name = Card.suit_names[self.suit]
        return f'{rank_name} of {suit_name}' 

وقتی یک Card را چاپ می‌کنیم، پایتون متد __str__ را فراخوانی می‌کند تا نمایشی قابل‌خواندن برای انسان بگیرد.

print(queen)
Queen of Diamonds

(در کتاب اصلی اینجا نمودار زیر آمده است: شیء کلاس Card که نوعش type است و متغیرهای کلاس suit_names و rank_names را دارد، و شیء queen که نمونه‌ای از Card است و صفت‌های suit=1 و rank=12 را دارد. برای صرفه‌جویی در فضا، محتوای suit_names و rank_names رسم نشده است.)

Card یک شیء کلاس است، پس نوعش type است. queen نمونه‌ای از Card است، پس نوعش Card است.

هر نمونه‌ی Card صفت‌های suit و rank خودش را دارد، اما فقط یک شیء کلاس Card و فقط یک نسخه از متغیرهای کلاس suit_names و rank_names وجود دارد.

۱۷.۴. مقایسه‌ی ورق‌ها

فرض کنید یک شیء Card دوم با همان خال و رتبه بسازیم.

queen2 = Card(1, 12)
print(queen2)
Queen of Diamonds

اگر از عملگر == برای مقایسه‌شان استفاده کنیم، بررسی می‌کند که queen و queen2 به یک شیء اشاره می‌کنند یا نه.

queen == queen2
False

اشاره نمی‌کنند، پس False برمی‌گرداند. می‌توانیم این رفتار را با تعریف متد ویژه‌ی __eq__ تغییر دهیم.

%%add_method_to Card

    def __eq__(self, other):
        return self.suit == other.suit and self.rank == other.rank

__eq__ دو شیء Card را به‌عنوان پارامتر می‌گیرد و اگر خال و رتبه‌ی یکسان داشته باشند True برمی‌گرداند، حتی اگر یک شیء نباشند. به عبارت دیگر، بررسی می‌کند که آیا هم‌ارزند، حتی اگر یکسان نباشند.

وقتی عملگر == را با شیءهای Card به کار می‌بریم، پایتون متد __eq__ را فراخوانی می‌کند.

queen == queen2
True

به‌عنوان آزمون دوم، ورقی با همان خال و رتبه‌ی متفاوت می‌سازیم.

six = Card(1, 6)
print(six)
6 of Diamonds

می‌توانیم تأیید کنیم که queen و six هم‌ارز نیستند.

queen == six
False

اگر از عملگر != استفاده کنیم، پایتون متد ویژه‌ای به نام __ne__ را، در صورت وجود، احضار می‌کند. در غیر این صورت __eq__ را احضار می‌کند و نتیجه را معکوس می‌کند – پس اگر __eq__ مقدار True برگرداند، نتیجه‌ی عملگر != برابر False است.

queen != queen2
False
queen != six
True

حالا فرض کنید می‌خواهیم دو ورق را مقایسه کنیم تا ببینیم کدام بزرگ‌تر است. اگر از یکی از عملگرهای رابطه‌ای استفاده کنیم، TypeError می‌گیریم.

%%expect TypeError

queen < queen2
TypeError: '<' not supported between instances of 'Card' and 'Card'

برای تغییر رفتار عملگر <، می‌توانیم متد ویژه‌ای به نام __lt__ تعریف کنیم، که مخفف «less than» (کوچک‌تر از) است. برای این مثال، فرض می‌کنیم خال مهم‌تر از رتبه است – پس همه‌ی پیک‌ها از همه‌ی دل‌ها برترند، که از همه‌ی خشت‌ها برترند و همین‌طور الی آخر. اگر دو ورق خال یکسان داشته باشند، آن‌که رتبه‌ی بالاتری دارد برنده است.

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

%%add_method_to Card

    def to_tuple(self):
        return (self.suit, self.rank)

می‌توانیم از این متد برای نوشتن __lt__ استفاده کنیم.

%%add_method_to Card

    def __lt__(self, other):
        return self.to_tuple() < other.to_tuple()

مقایسه‌ی چندتایی‌ها عنصرهای اول هر چندتایی را، که خال‌ها را نشان می‌دهند، مقایسه می‌کند. اگر یکسان بودند، عنصرهای دوم، که رتبه‌ها را نشان می‌دهند، مقایسه می‌شوند.

حالا اگر از عملگر < استفاده کنیم، متد __lt__ احضار می‌شود.

six < queen
True

اگر از عملگر > استفاده کنیم، متد ویژه‌ای به نام __gt__ را، در صورت وجود، احضار می‌کند. در غیر این صورت __lt__ را با آرگومان‌ها به ترتیب معکوس احضار می‌کند.

queen < queen2
False
queen > queen2
False

سرانجام، اگر از عملگر <= استفاده کنیم، متد ویژه‌ای به نام __le__ را احضار می‌کند.

%%add_method_to Card

    def __le__(self, other):
        return self.to_tuple() <= other.to_tuple()

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

queen <= queen2
True
queen <= six
False

اگر از عملگر >= استفاده کنیم، در صورت وجود از __ge__ استفاده می‌کند. در غیر این صورت، __le__ را با آرگومان‌ها به ترتیب معکوس احضار می‌کند.

queen >= six
True

همان‌طور که این متدها را تعریف کرده‌ایم، کامل‌اند به این معنا که می‌توانیم هر دو شیء Card را مقایسه کنیم، و سازگار‌اند به این معنا که نتیجه‌ی عملگرهای متفاوت با هم تناقض ندارند. با این دو ویژگی، می‌توانیم بگوییم شیءهای Card ترتیب کامل (totally ordered) دارند. و این یعنی، همان‌طور که به‌زودی خواهیم دید، می‌توان آن‌ها را مرتب کرد.

۱۷.۵. دسته‌های ورق (Decks)

حالا که شیءهایی داریم که ورق‌ها را نشان می‌دهند، شیءهایی تعریف می‌کنیم که دسته‌ها را نشان می‌دهند. این تعریف کلاس Deck است که یک متد __init__ دارد که فهرستی از شیءهای Card را به‌عنوان پارامتر می‌گیرد و به صفتی به نام cards نسبت می‌دهد.

class Deck:

    def __init__(self, cards):
        self.cards = cards

برای ساختن فهرستی که ۵۲ ورق یک دسته‌ی استاندارد را دارد، از متد ایستای زیر استفاده می‌کنیم.

%%add_method_to Deck

    def make_cards():
        cards = []
        for suit in range(4):
            for rank in range(2, 15):
                card = Card(suit, rank)
                cards.append(card)
        return cards

در make_cards، حلقه‌ی بیرونی خال‌ها را از 0 تا 3 برمی‌شمارد. حلقه‌ی درونی رتبه‌ها را از 2 تا 14 برمی‌شمارد – که 14 آسی را نشان می‌دهد که از شاه بالاتر است. هر تکرار یک Card جدید با خال و رتبه‌ی فعلی می‌سازد و به cards اضافه می‌کند.

این‌طور فهرستی از ورق‌ها و یک شیء Deck که آن را دارد می‌سازیم.

cards = Deck.make_cards()
deck = Deck(cards)
len(deck.cards)
52

همان‌طور که قصد داشتیم ۵۲ ورق دارد.

۱۷.۶. چاپ دسته

این یک متد __str__ برای Deck است.

%%add_method_to Deck

    def __str__(self):
        res = []
        for card in self.cards:
            res.append(str(card))
        return '\n'.join(res)

این متد روشی کارآمد برای انباشتن یک رشته‌ی بزرگ را نشان می‌دهد – ساختن فهرستی از رشته‌ها و بعد استفاده از متد رشته‌ی join.

این متد را با دسته‌ای آزمون می‌کنیم که فقط دو ورق دارد.

small_deck = Deck([queen, six])

اگر str را فراخوانی کنیم، __str__ احضار می‌شود.

str(small_deck)
'Queen of Diamonds\n6 of Diamonds'

وقتی Jupyter یک رشته را نمایش می‌دهد، شکل «نمایشی» (representational) رشته را نشان می‌دهد که خط جدید را با دنباله‌ی \n نشان می‌دهد.

اما اگر نتیجه را چاپ کنیم، Jupyter شکل «چاپی» (printable) را نشان می‌دهد که خط جدید را به‌صورت فاصله‌ی خالی چاپ می‌کند.

print(small_deck)
Queen of Diamonds
6 of Diamonds

پس ورق‌ها در خط‌های جداگانه ظاهر می‌شوند.

۱۷.۷. اضافه، حذف، بُر زدن و مرتب‌سازی

برای پخش کردن ورق‌ها، متدی می‌خواهیم که یک ورق را از دسته بردارد و برگرداند. متد pop فهرست راه مناسبی برای این کار است.

%%add_method_to Deck

    def take_card(self):
        return self.cards.pop()

این‌طور از آن استفاده می‌کنیم.

card = deck.take_card()
print(card)
Ace of Spades

می‌توانیم تأیید کنیم که 51 ورق در دسته باقی مانده است.

len(deck.cards)
51

برای اضافه کردن ورق، می‌توانیم از متد append فهرست استفاده کنیم.

%%add_method_to Deck

    def put_card(self, card):
        self.cards.append(card)

برای مثال، می‌توانیم ورقی را که همین الان برداشتیم برگردانیم.

deck.put_card(card)
len(deck.cards)
52

برای بُر زدن دسته، می‌توانیم از تابع shuffle ماژول random استفاده کنیم:

import random
# This cell initializes the random number generator so we
# always get the same results.

random.seed(3)
%%add_method_to Deck
            
    def shuffle(self):
        random.shuffle(self.cards)

اگر دسته را بُر بزنیم و چند ورق اول را چاپ کنیم، می‌بینیم که ترتیب مشخصی ندارند.

deck.shuffle()
for card in deck.cards[:4]:
    print(card)
2 of Diamonds
4 of Hearts
5 of Clubs
8 of Diamonds

برای مرتب کردن ورق‌ها، می‌توانیم از متد sort فهرست استفاده کنیم، که عنصرها را «درجا» (in place) مرتب می‌کند – یعنی فهرست را تغییر می‌دهد به‌جای اینکه فهرست جدیدی بسازد.

%%add_method_to Deck
            
    def sort(self):
        self.cards.sort()

وقتی sort را احضار می‌کنیم، برای مقایسه‌ی ورق‌ها از متد __lt__ استفاده می‌کند.

deck.sort()

اگر چند ورق اول را چاپ کنیم، می‌توانیم تأیید کنیم که به ترتیب صعودی‌اند.

for card in deck.cards[:4]:
    print(card)
2 of Clubs
3 of Clubs
4 of Clubs
5 of Clubs

در این مثال، Deck.sort کاری جز احضار list.sort نمی‌کند. به این نوع واگذاری مسئولیت تفویض (delegation) می‌گویند.

۱۷.۸. والدین و فرزندان

وراثت توانایی تعریف کلاس جدیدی است که نسخه‌ی تغییریافته‌ی یک کلاس موجود باشد. برای مثال، فرض کنید می‌خواهیم کلاسی داشته باشیم که یک «دست» (hand) را نشان دهد، یعنی ورق‌هایی که یک بازیکن در دست دارد.

  • یک دست شبیه یک دسته است – هر دو از مجموعه‌ای از ورق‌ها تشکیل شده‌اند، و هر دو عملیاتی مثل اضافه و حذف ورق لازم دارند.

  • یک دست با یک دسته فرق هم دارد – عملیاتی هست که برای دست‌ها می‌خواهیم و برای دسته معنا ندارد. برای مثال، در پوکر ممکن است دو دست را مقایسه کنیم تا ببینیم کدام برنده است. در بریج (bridge)، ممکن است برای بستن یک مزایده (bid) امتیازی برای یک دست حساب کنیم.

این رابطه بین کلاس‌ها – که یکی نسخه‌ی تخصصی‌شده‌ی دیگری است – برای وراثت مناسب است.

برای تعریف کلاس جدیدی که بر پایه‌ی یک کلاس موجود است، نام کلاس موجود را داخل پرانتز می‌گذاریم.

class Hand(Deck):
    """Represents a hand of playing cards."""

این تعریف نشان می‌دهد که Hand از Deck ارث می‌برد، یعنی شیءهای Hand می‌توانند به متدهای تعریف‌شده در Deck، مثل take_card و put_card، دسترسی داشته باشند.

Hand متد __init__ را هم از Deck به ارث می‌برد، اما اگر __init__ را در کلاس Hand تعریف کنیم، آن را که در کلاس Deck است بازنویسی (override) می‌کند.

%%add_method_to Hand

    def __init__(self, label=''):
        self.label = label
        self.cards = []

این نسخه از __init__ یک رشته‌ی اختیاری به‌عنوان پارامتر می‌گیرد و همیشه با فهرست خالی از ورق‌ها شروع می‌کند. وقتی یک Hand می‌سازیم، پایتون این متد را احضار می‌کند، نه آن را که در Deck است – که می‌توانیم با بررسی اینکه نتیجه صفت label دارد تأیید کنیم.

hand = Hand('player 1')
hand.label
'player 1'

برای پخش کردن یک ورق، می‌توانیم از take_card برای برداشتن ورق از یک Deck و از put_card برای اضافه کردن آن به یک Hand استفاده کنیم.

deck = Deck(cards)
card = deck.take_card()
hand.put_card(card)
print(hand)
Ace of Spades

این کد را در یک متد Deck به نام move_cards کپسوله می‌کنیم.

%%add_method_to Deck

    def move_cards(self, other, num):
        for i in range(num):
            card = self.take_card()
            other.put_card(card)

این متد چندریخت است – یعنی با بیش از یک نوع کار می‌کند: self و other هر کدام می‌توانند Hand یا Deck باشند. پس می‌توانیم از این متد برای پخش یک ورق از Deck به Hand، از یک Hand به دیگری، یا از یک Hand به Deck استفاده کنیم.

وقتی کلاس جدیدی از کلاس موجودی ارث می‌برد، کلاس موجود والد (parent) و کلاس جدید فرزند (child) نامیده می‌شود. به‌طور کلی:

  • نمونه‌های کلاس فرزند باید همه‌ی صفت‌های کلاس والد را داشته باشند، اما می‌توانند صفت‌های اضافی هم داشته باشند.

  • کلاس فرزند باید همه‌ی متدهای کلاس والد را داشته باشد، اما می‌تواند متدهای اضافی هم داشته باشد.

  • اگر کلاس فرزند متدی از کلاس والد را بازنویسی کند، متد جدید باید پارامترهای یکسان بگیرد و نتیجه‌ای سازگار برگرداند.

این مجموعه قواعد «اصل جایگزینی لیسکوف» (Liskov substitution principle) نامیده می‌شود، به نام Barbara Liskov، دانشمند علوم کامپیوتر.

اگر این قواعد را رعایت کنید، هر تابع یا متدی که برای کار با نمونه‌ای از کلاس والد، مثل Deck، طراحی شده، با نمونه‌های کلاس فرزند، مثل Hand، هم کار خواهد کرد. اگر این قواعد را نقض کنید، کدتان مثل قصر ورقی فرو می‌ریزد (house of cards؛ ببخشید، بازی با کلمات شد).

۱۷.۹. تخصصی‌سازی (Specialization)

کلاسی به نام BridgeHand می‌سازیم که یک دست در بریج – بازی ورق پرطرفدار – را نشان می‌دهد. از Hand ارث می‌بریم و متد جدیدی به نام high_card_point_count اضافه می‌کنیم که یک دست را با روش «امتیاز ورق‌های بالا» ارزیابی می‌کند، یعنی برای ورق‌های بالای دست امتیاز جمع می‌کند.

این تعریف کلاسی است که یک دیکشنری را به‌عنوان متغیر کلاس دارد که از نام ورق‌ها به امتیازشان نگاشت می‌کند.

class BridgeHand(Hand):
    """Represents a bridge hand."""

    hcp_dict = {
        'Ace': 4,
        'King': 3,
        'Queen': 2,
        'Jack': 1,
    }

با داشتن رتبه‌ی یک ورق، مثلاً 12، می‌توانیم از Card.rank_names برای گرفتن نمایش رشته‌ای رتبه و بعد از hcp_dict برای گرفتن امتیازش استفاده کنیم.

rank = 12
rank_name = Card.rank_names[rank]
score = BridgeHand.hcp_dict.get(rank_name, 0)
rank_name, score
('Queen', 2)

متد زیر ورق‌های یک BridgeHand را پیمایش می‌کند و امتیازشان را جمع می‌زند.

%%add_method_to BridgeHand

    def high_card_point_count(self):
        count = 0
        for card in self.cards:
            rank_name = Card.rank_names[card.rank]
            count += BridgeHand.hcp_dict.get(rank_name, 0)
        return count
# This cell makes a fresh Deck and 
# initializes the random number generator

cards = Deck.make_cards()
deck = Deck(cards)
random.seed(3)

برای آزمونش، دستی با پنج ورق پخش می‌کنیم – یک دست بریج معمولاً سیزده ورق دارد، اما آزمودن کد با مثال‌های کوچک آسان‌تر است.

hand = BridgeHand('player 2')

deck.shuffle()
deck.move_cards(hand, 5)
print(hand)
4 of Diamonds
King of Hearts
10 of Hearts
10 of Clubs
Queen of Diamonds

و این هم امتیاز کل شاه و بی‌بی.

hand.high_card_point_count()
5

BridgeHand متغیرها و متدهای Hand را به ارث می‌برد و یک متغیر کلاس و یک متد مخصوص بریج اضافه می‌کند. این شیوه‌ی استفاده از وراثت تخصصی‌سازی (specialization) نامیده می‌شود چون کلاس جدیدی تعریف می‌کند که برای کاربرد ویژه‌ای، مثل بازی بریج، تخصصی شده است.

۱۷.۱۰. اشکال‌زدایی

وراثت ویژگی مفیدی است. بعضی برنامه‌ها که بدون وراثت تکراری می‌شدند، با آن مختصرتر نوشته می‌شوند. همچنین وراثت می‌تواند استفاده‌ی مجدد از کد را آسان کند، چون می‌توانید رفتار کلاس والد را بدون تغییر آن سفارشی کنید. در بعضی موارد ساختار وراثت ساختار طبیعی مسئله را بازتاب می‌دهد، که طراحی را قابل‌فهم‌تر می‌کند.

از سوی دیگر، وراثت می‌تواند خواندن برنامه‌ها را دشوار کند. وقتی متدی احضار می‌شود، گاهی روشن نیست که تعریفش را کجا پیدا کنیم – ممکن است کد مربوط در چند ماژول پخش شده باشد.

هر وقت درباره‌ی جریان اجرای برنامه‌تان مطمئن نیستید، ساده‌ترین راه‌حل افزودن دستورهای print در ابتدای متدهای مربوط است. اگر Deck.shuffle پیامی چاپ کند، مثلاً Running Deck.shuffle، با اجرای برنامه جریان اجرا ردیابی می‌شود.

به‌عنوان یک جایگزین، می‌توانید از تابع زیر استفاده کنید که یک شیء و نام یک متد (به‌صورت رشته) را می‌گیرد و کلاسی را برمی‌گرداند که تعریف متد را فراهم می‌کند.

def find_defining_class(obj, method_name):
    """Find the class where the given method is defined."""
    for typ in type(obj).mro():
        if method_name in vars(typ):
            return typ
    return f'Method {method_name} not found.'

find_defining_class از متد mro استفاده می‌کند تا فهرست شیءهای کلاس (نوع‌ها) را که برای یافتن متدها جست‌وجو می‌شوند بگیرد. «MRO» مخفف «method resolution order» (ترتیب حل متد) است، یعنی دنباله‌ی کلاس‌هایی که پایتون برای «حل» نام یک متد جست‌وجو می‌کند – یعنی پیدا کردن شیء تابعی که نام به آن اشاره می‌کند.

به‌عنوان مثال، یک BridgeHand نمونه‌سازی می‌کنیم و بعد کلاس تعریف‌کننده‌ی shuffle را پیدا می‌کنیم.

hand = BridgeHand('player 3')
find_defining_class(hand, 'shuffle')
__main__.Deck

متد shuffle شیء BridgeHand همانی است که در Deck است.

(نکته: در اجرای من در یک اسکریپت معمولی، خروجی به شکل <class '__main__.Deck'> ظاهر می‌شود؛ شکل نمایش در Jupyter کمی فرق دارد.)

۱۷.۱۱. واژه‌نامه

وراثت (inheritance): توانایی تعریف کلاس جدیدی که نسخه‌ی تغییریافته‌ی یک کلاس تعریف‌شده‌ی قبلی باشد.

کدگذاری (encode): نمایش یک مجموعه از مقدارها با مجموعه‌ی دیگری از مقدارها با ساختن نگاشتی بین آن‌ها.

متغیر کلاس (class variable): متغیری که داخل تعریف کلاس، اما نه داخل هیچ متدی، تعریف می‌شود.

ترتیب کامل (totally ordered): مجموعه‌ای از شیءها ترتیب کامل دارد اگر بتوانیم هر دو عنصر را مقایسه کنیم و نتیجه‌ها سازگار باشند.

تفویض (delegation): وقتی یک متد مسئولیت را به متد دیگری واگذار می‌کند تا بیشتر یا همه‌ی کار را انجام دهد.

کلاس والد (parent class): کلاسی که از آن ارث برده می‌شود.

کلاس فرزند (child class): کلاسی که از کلاس دیگری ارث می‌برد.

تخصصی‌سازی (specialization): شیوه‌ای از استفاده از وراثت برای ساختن کلاس جدیدی که نسخه‌ی تخصصی‌شده‌ی یک کلاس موجود است.

۱۷.۱۲. تمرین‌ها

# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.

%xmode Verbose

از یک دستیار مجازی بپرسید

وقتی خوب پیش برود، برنامه‌نویسی شیءگرا می‌تواند برنامه‌ها را خواناتر، آزمون‌پذیرتر و قابل‌استفاده‌ی مجددتر کند. اما می‌تواند برنامه‌ها را پیچیده و نگهداری‌شان را سخت هم بکند. در نتیجه، OOP موضوعی مناقشه‌برانگیز است – بعضی آن را دوست دارند و بعضی نه.

برای یادگیری بیشتر درباره‌ی این موضوع، از یک دستیار مجازی بپرسید:

  • What are some pros and cons of object-oriented programming? (مزایا و معایب برنامه‌نویسی شیءگرا چیست؟)

  • What does it mean when people say “favor composition over inheritance”? (وقتی مردم می‌گویند «ترکیب را بر وراثت ترجیح بده» یعنی چه؟)

  • What is the Liskov substitution principle? (اصل جایگزینی لیسکوف چیست؟)

  • Is Python an object-oriented language? (آیا پایتون یک زبان شیءگرا است؟)

  • What are the requirements for a set to be totally ordered? (شرط‌های ترتیب کامل داشتن یک مجموعه چیست؟)

و مثل همیشه، استفاده از یک دستیار مجازی برای کمک در تمرین‌های زیر را در نظر بگیرید.

تمرین ۱

در بریج قراردادی (contract bridge)، «دست» (trick) دوری از بازی است که در آن هر یک از چهار بازیکن یک ورق بازی می‌کند. برای نمایش آن ورق‌ها، کلاسی تعریف می‌کنیم که از Deck ارث می‌برد.

class Trick(Deck):
    """Represents a trick in contract bridge."""

به‌عنوان مثال، این دست را در نظر بگیرید که در آن بازیکن اول با ۳ خشت شروع می‌کند، یعنی خشت «خال آغازین» (led suit) است. بازیکن دوم و سوم «از خال پیروی می‌کنند» (follow suit)، یعنی ورقی با خال آغازین بازی می‌کنند. بازیکن چهارم ورقی با خال متفاوت بازی می‌کند، یعنی نمی‌تواند این دست را ببرد. پس برنده‌ی این دست بازیکن سوم است، چون بالاترین ورق در خال آغازین را بازی کرده است.

cards = [Card(1, 3),
         Card(1, 10),
         Card(1, 12),
         Card(2, 13)]
trick = Trick(cards)
print(trick)
3 of Diamonds
10 of Diamonds
Queen of Diamonds
King of Hearts

متدی برای Trick به نام find_winner بنویسید که ورق‌های Trick را پیمایش کند و اندیس ورقی را که می‌برد برگرداند. در مثال قبل، اندیس ورق برنده 2 است.

می‌توانید از این طرح اولیه شروع کنید.

%%add_method_to Trick

    def find_winner(self):
        return 0
# Solution goes here

اگر متدتان را با مثال قبل آزمون کنید، اندیس ورق برنده باید 2 باشد.

trick.find_winner()

تمرین ۲

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

class PokerHand(Hand):
    """Represents a poker hand."""

    def get_suit_counts(self):
        counter = {}
        for card in self.cards:
            key = card.suit
            counter[key] = counter.get(key, 0) + 1
        return counter
    
    def get_rank_counts(self):
        counter = {}
        for card in self.cards:
            key = card.rank
            counter[key] = counter.get(key, 0) + 1
        return counter    

PokerHand دو متد دارد که در تمرین‌ها کمک می‌کنند.

  • get_suit_counts ورق‌های PokerHand را پیمایش می‌کند، تعداد ورق‌های هر خال را می‌شمارد و دیکشنری‌ای برمی‌گرداند که از هر کد خال به تعداد دفعاتی که ظاهر می‌شود نگاشت می‌کند.

  • get_rank_counts همین کار را با رتبه‌های ورق‌ها انجام می‌دهد و دیکشنری‌ای برمی‌گرداند که از هر کد رتبه به تعداد دفعاتی که ظاهر می‌شود نگاشت می‌کند.

همه‌ی تمرین‌های بعدی را می‌توان فقط با امکانات پایتونی که تا حالا یاد گرفته‌ایم انجام داد، اما بعضی از آن‌ها دشوارتر از بیشتر تمرین‌های قبلی‌اند. تشویقتان می‌کنم از یک دستیار مجازی کمک بگیرید.

برای مسئله‌هایی مثل این، اغلب خوب جواب می‌دهد که درباره‌ی راهبردها و الگوریتم‌ها توصیه‌ی کلی بخواهید. بعد می‌توانید یا خودتان کد را بنویسید یا کد بخواهید. اگر کد می‌خواهید، شاید بخواهید تعریف کلاس‌های مربوط را در متن درخواست بگنجانید.

به‌عنوان اولین تمرین، متدی به نام has_flush می‌نویسیم که بررسی می‌کند دست «فلاش» (flush) دارد یا نه – یعنی دست‌کم پنج ورق با یک خال دارد یا نه.

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

می‌توانید از این طرح اولیه شروع کنید.

%%add_method_to PokerHand

    def has_flush(self):
        """Checks whether this hand has a flush."""
        return False
# Solution goes here

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

good_hand = PokerHand('good_hand')

suit = 0
for rank in range(10, 15):
    card = Card(suit, rank)
    good_hand.put_card(card)
    
print(good_hand)
10 of Clubs
Jack of Clubs
Queen of Clubs
King of Clubs
Ace of Clubs

اگر get_suit_counts را احضار کنیم، می‌توانیم تأیید کنیم که کد خال 0 پنج بار ظاهر می‌شود. (کتاب اینجا به اشتباه «کد رتبه» نوشته است؛ منظور کد خال است.)

good_hand.get_suit_counts()
{0: 5}

پس has_flush باید True برگرداند.

good_hand.has_flush()
True

به‌عنوان آزمون دوم، دستی با سه گشنیز و دو خال دیگر می‌سازیم.

cards = [Card(0, 2),
         Card(0, 3),
         Card(2, 4),
         Card(3, 5),
         Card(0, 7),
        ]

bad_hand = PokerHand('bad hand')
for card in cards:
    bad_hand.put_card(card)
    
print(bad_hand)
2 of Clubs
3 of Clubs
4 of Hearts
5 of Spades
7 of Clubs

پس has_flush باید False برگرداند.

bad_hand.has_flush()
False

تمرین ۳

متدی به نام has_straight بنویسید که بررسی کند دست «استریت» (straight) دارد یا نه، یعنی مجموعه‌ای از پنج ورق با رتبه‌های متوالی. برای مثال، اگر دستی رتبه‌های 5، 6، 7، 8 و 9 را داشته باشد، یک استریت دارد.

آس می‌تواند قبل از دو یا بعد از شاه بیاید، پس Ace، 2، 3، 4، 5 یک استریت است و 10، Jack، Queen، King، Ace هم همین‌طور. اما یک استریت نمی‌تواند «دور بزند»، پس King، Ace، 2، 3، 4 استریت نیست.

می‌توانید از این طرح اولیه شروع کنید. چند خط کد دارد که تعداد آس‌ها – که با کد 1 یا 14 نشان داده می‌شوند – را می‌شمارد و مجموع را در هر دو جا در شمارنده ذخیره می‌کند.

%%add_method_to PokerHand

    def has_straight(self, n=5):
        """Checks whether this hand has a straight with at least `n` cards."""
        counter = self.get_rank_counts()
        aces = counter.get(1, 0) + counter.get(14, 0)
        counter[1] = aces
        counter[14] = aces
        
        return False
# Solution goes here

good_hand، که برای تمرین قبل ساختیم، یک استریت دارد. اگر از get_rank_counts استفاده کنیم، می‌توانیم تأیید کنیم که دست‌کم یک ورق با هر یک از پنج رتبه‌ی متوالی دارد.

good_hand.get_rank_counts()
{10: 1, 11: 1, 12: 1, 13: 1, 14: 1}

پس has_straight باید True برگرداند.

good_hand.has_straight()
True

bad_hand استریت ندارد، پس has_straight باید False برگرداند.

bad_hand.has_straight()
False

تمرین ۴

یک دست «استریت فلاش» (straight flush) دارد اگر مجموعه‌ای از پنج ورق داشته باشد که هم استریت و هم فلاش باشند – یعنی پنج ورق از یک خال با رتبه‌های متوالی. متدی برای PokerHand بنویسید که بررسی کند دست استریت فلاش دارد یا نه.

می‌توانید از این طرح اولیه شروع کنید.

%%add_method_to PokerHand

    def has_straightflush(self):
        """Check whether this hand has a straight flush."""
        return False
# Solution goes here
# Solution goes here

از مثال‌های زیر برای آزمودن متدتان استفاده کنید.

good_hand.has_straightflush()     # should return True
bad_hand.has_straightflush()     # should return False

توجه کنید که بررسی اینکه دست یک استریت و یک فلاش دارد کافی نیست. برای دیدن دلیلش، دست زیر را در نظر بگیرید.

from copy import deepcopy

straight_and_flush = deepcopy(bad_hand)
straight_and_flush.put_card(Card(0, 6))
straight_and_flush.put_card(Card(0, 9))
print(straight_and_flush)
2 of Clubs
3 of Clubs
4 of Hearts
5 of Spades
7 of Clubs
6 of Clubs
9 of Clubs

این دست یک استریت و یک فلاش دارد، اما یک پنج ورق نیستند.

straight_and_flush.has_straight(), straight_and_flush.has_flush()
(True, True)

پس استریت فلاش ندارد.

straight_and_flush.has_straightflush()    # should return False

تمرین ۵

یک دست پوکر «جفت» (pair) دارد اگر دو یا بیش از دو ورق با رتبه‌ی یکسان داشته باشد. متدی برای PokerHand بنویسید که بررسی کند دست جفت دارد یا نه.

می‌توانید از این طرح اولیه شروع کنید.

%%add_method_to PokerHand

    def has_pair(self):
        return True
# Solution goes here
# Solution goes here

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

pair = deepcopy(bad_hand)
pair.put_card(Card(1, 2))
print(pair)
2 of Clubs
3 of Clubs
4 of Hearts
5 of Spades
7 of Clubs
2 of Diamonds
pair.has_pair()    # should return True
bad_hand.has_pair()    # should return False
good_hand.has_pair()   # should return False

تمرین ۶

یک دست «فول‌هاوس» (full house) دارد اگر سه ورق از یک رتبه و دو ورق از رتبه‌ی دیگری داشته باشد. متدی برای PokerHand بنویسید که بررسی کند دست فول‌هاوس دارد یا نه.

می‌توانید از این طرح اولیه شروع کنید.

%%add_method_to PokerHand

    def has_full_house(self):
        return False
# Solution goes here

می‌توانید از این دست برای آزمودن متدتان استفاده کنید.

boat = deepcopy(pair)
boat.put_card(Card(2, 2))
boat.put_card(Card(2, 3))
print(boat)
2 of Clubs
3 of Clubs
4 of Hearts
5 of Spades
7 of Clubs
2 of Diamonds
2 of Hearts
3 of Hearts
boat.has_full_house()     # should return True
pair.has_full_house()     # should return False
good_hand.has_full_house()     # should return False

تمرین ۷

این تمرین حکایتی هشداردهنده درباره‌ی خطای رایجی است که اشکال‌زدایی‌اش می‌تواند دشوار باشد. تعریف کلاس زیر را در نظر بگیرید.

class Kangaroo:
    """A Kangaroo is a marsupial."""
    
    def __init__(self, name, contents=[]):
        """Initialize the pouch contents.

        name: string
        contents: initial pouch contents.
        """
        self.name = name
        self.contents = contents

    def __str__(self):
        """Return a string representaion of this Kangaroo.
        """
        t = [ self.name + ' has pouch contents:' ]
        for obj in self.contents:
            s = '    ' + object.__str__(obj)
            t.append(s)
        return '\n'.join(t)

    def put_in_pouch(self, item):
        """Adds a new item to the pouch contents.

        item: object to be added
        """
        self.contents.append(item)

__init__ دو پارامتر می‌گیرد: name الزامی است، اما contents اختیاری است – اگر داده نشود، مقدار پیش‌فرض یک فهرست خالی است.

__str__ یک نمایش رشته‌ای از شیء برمی‌گرداند که نام و محتوای کیسه را دارد.

put_in_pouch هر شیئی را می‌گیرد و به contents اضافه می‌کند.

حالا ببینیم این کلاس چطور کار می‌کند. دو شیء Kangaroo با نام‌های 'Kanga' و 'Roo' می‌سازیم.

kanga = Kangaroo('Kanga')
roo = Kangaroo('Roo')

به کیسه‌ی Kanga دو رشته و Roo را اضافه می‌کنیم.

kanga.put_in_pouch('wallet')
kanga.put_in_pouch('car keys')
kanga.put_in_pouch(roo)

اگر kanga را چاپ کنیم، به نظر می‌رسد همه‌چیز درست کار کرده.

print(kanga)
Kanga has pouch contents:
    'wallet'
    'car keys'
    <__main__.Kangaroo object at 0x7f169807ead0>

اما اگر roo را چاپ کنیم چه می‌شود؟

print(roo)
Roo has pouch contents:
    'wallet'
    'car keys'
    <__main__.Kangaroo object at 0x7f169807ead0>

(آدرس حافظه در اجرای شما فرق می‌کند.)

کیسه‌ی Roo همان محتوای کیسه‌ی Kanga را دارد، از جمله ارجاعی به خود roo!

ببینید می‌توانید بفهمید چه اشتباهی رخ داده. بعد از یک دستیار مجازی بپرسید “What’s wrong with the following program?” (مشکل برنامه‌ی زیر چیست؟) و تعریف Kangaroo را بچسبانید.