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

چند نکتهٔ اضافی پایتون (فصل ۱۸ Think Python)

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

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

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

توضیح‌ها:

  • خروجی‌های زیر کدها را با اجرای همین کدها (با پایتون ۳٫۱۳) به دست آورده‌ام. ترتیب نمایش عنصرهای یک set به رشته‌ها و نسخه‌ی پایتون بستگی دارد و ممکن است در اجرای شما فرق کند. متن خطاها نیز بین نسخه‌های پایتون کمی متفاوت است.
  • در مثال‌ها، عنوان فیلم (که در متن اصلی به یک اثر سینمایی اشاره می‌کرد) را با جمله‌ی خنثی 'once upon a time in a small town' عوض کرده‌ام. همچنین کلمه‌ی 'monty' در مثال any به 'mount' و چندتایی ('into', 'the') با رشته‌ی 'woods' به ('out', 'of') با 'sight' تغییر کرده است. این تغییرها روی منطق مثال‌ها اثری ندارند.
  • %%expect و %%add_method_to فقط در Jupyter و محیط کتاب کار می‌کنند. اولی اعلام می‌کند که انتظار یک خطای مشخص را داریم و دومی متدی را به کلاسی که قبلاً تعریف شده اضافه می‌کند.

این‌ها نسخه‌هایی از کلاس‌های Card، Deck و Hand از فصل ۱۷ هستند که در بعضی مثال‌های این فصل به کار می‌بریم.

class Card:
    suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
    rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7', 
                  '8', '9', '10', 'Jack', 'Queen', 'King', 'Ace']
    
    def __init__(self, suit, rank):
        self.suit = suit
        self.rank = rank

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

class Deck:
    def __init__(self, cards):
        self.cards = cards
        
    def __str__(self):
        res = []
        for card in self.cards:
            res.append(str(card))
        return '\n'.join(res)
    
    def make_cards():
        cards = []
        for suit in range(4):
            for rank in range(2, 15):
                card = Card(suit, rank)
                cards.append(card)
        return cards
    
    def shuffle(self):
        random.shuffle(self.cards)
        
    def pop_card(self):
        return self.cards.pop()
    
    def add_card(self, card):
        self.cards.append(card)
class Hand(Deck):
    def __init__(self, label=''):
        self.label = label
        self.cards = []

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

حالا می‌خواهم برگردم سراغ بعضی چیزهای خوبی که جا مانده بودند. پایتون تعدادی امکان دارد که واقعاً ضروری نیستند – بدون آن‌ها هم می‌توانید کد خوب بنویسید – ولی با آن‌ها می‌توانید کدی بنویسید که مختصرتر، خواناتر یا کارآمدتر است، و گاهی هر سه.

۱۸.۱. مجموعه‌ها (Sets)

پایتون کلاسی به نام set دارد که مجموعه‌ای از عنصرهای یکتا را نشان می‌دهد. برای ساختن مجموعه‌ی خالی، می‌توانیم شیء کلاس را مثل یک تابع به کار ببریم.

s1 = set()
s1
set()

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

s1.add('a')
s1.add('b')
s1
{'a', 'b'}

یا می‌توانیم هر نوع دنباله‌ای را به set بدهیم.

s2 = set('acd')
s2
{'a', 'c', 'd'}

یک عنصر فقط یک بار می‌تواند در set ظاهر شود. اگر عنصری را که از قبل وجود دارد اضافه کنید، تأثیری ندارد.

s1.add('a')
s1
{'a', 'b'}

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

set('banana')
{'a', 'b', 'n'}

(ترتیب چاپ عنصرها در set معنایی ندارد؛ ممکن است در اجرای شما به ترتیب دیگری ظاهر شوند.)

بعضی از تمرین‌های این کتاب را می‌توان با مجموعه‌ها مختصر و کارآمد انجام داد. برای مثال، این راه‌حل یکی از تمرین‌های فصل ۱۱ است که از یک دیکشنری برای بررسی اینکه آیا عنصر تکراری در یک دنباله وجود دارد استفاده می‌کند.

def has_duplicates(t):
    d = {}
    for x in t:
        d[x] = True
    return len(d) < len(t)

این نسخه عنصرهای t را به‌عنوان کلید به یک دیکشنری اضافه می‌کند و بعد بررسی می‌کند که آیا کلیدها از عنصرها کمترند. با مجموعه‌ها می‌توانیم همین تابع را این‌طور بنویسیم.

def has_duplicates(t):
    s = set(t)
    return len(s) < len(t)
has_duplicates('abba')
True

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

شیءهای set متدهایی برای عملیات مجموعه‌ای دارند. برای مثال، union اجتماع دو مجموعه را حساب می‌کند، که مجموعه‌ی جدیدی است شامل همه‌ی عنصرهایی که در هر یک از دو مجموعه هستند.

s1.union(s2)
{'a', 'b', 'c', 'd'}

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

s1 - s2
{'b'}

در فصل ۱۲ از دیکشنری‌ها برای پیدا کردن کلمه‌هایی که در یک سند هستند اما در فهرست کلمه‌ها نیستند استفاده کردیم. از تابع زیر استفاده کردیم که دو دیکشنری می‌گیرد و دیکشنری جدیدی برمی‌گرداند که فقط کلیدهایی از اولی را دارد که در دومی نیستند.

def subtract(d1, d2):
    res = {}
    for key in d1:
        if key not in d2:
            res[key] = d1[key]
    return res

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

# this cell creates a small example so we can run the following
# cell without loading the actual data

word_counter = {'word': 1}
word_list = ['word']
set(word_counter) - set(word_list)
set()

نتیجه مجموعه‌ای است شامل کلمه‌هایی از سند که در فهرست کلمه‌ها نیستند.

عملگرهای رابطه‌ای با مجموعه‌ها کار می‌کنند. برای مثال، <= بررسی می‌کند که آیا یک مجموعه زیرمجموعه‌ی مجموعه‌ی دیگری است، از جمله امکان اینکه مساوی باشند.

set('ab') <= set('abc')
True

با این عملگرها می‌توانیم از مجموعه‌ها برای انجام بعضی تمرین‌های فصل ۷ استفاده کنیم. برای مثال، این نسخه‌ای از uses_only است که حلقه به کار می‌برد.

def uses_only(word, available):
    for letter in word: 
        if letter not in available:
            return False
    return True

uses_only بررسی می‌کند که آیا همه‌ی حرف‌های word در available هستند. با مجموعه‌ها می‌توانیم آن را این‌طور بازنویسی کنیم.

def uses_only(word, available):
    return set(word) <= set(available)

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

۱۸.۲. شمارنده‌ها (Counters)

Counter شبیه مجموعه است، با این تفاوت که اگر عنصری بیش از یک بار ظاهر شود، Counter تعداد دفعات ظاهر شدنش را ثبت می‌کند. اگر با ایده‌ی ریاضی «چندمجموعه» (multiset) آشنا هستید، Counter راه طبیعی برای نمایش یک چندمجموعه است.

کلاس Counter در ماژولی به نام collections تعریف شده، پس باید آن را وارد کنید. بعد می‌توانید شیء کلاس را مثل تابع به کار ببرید و یک رشته، فهرست یا هر دنباله‌ی دیگری را به‌عنوان آرگومان بدهید.

from collections import Counter

counter = Counter('banana')
counter
Counter({'a': 3, 'n': 2, 'b': 1})

یک شیء Counter مثل دیکشنری است که هر کلید را به تعداد دفعات ظاهر شدنش نگاشت می‌کند. مثل دیکشنری‌ها، کلیدها باید درهم‌پذیر (hashable) باشند.

برخلاف دیکشنری‌ها، شیءهای Counter اگر به عنصری که ظاهر نمی‌شود دسترسی پیدا کنید استثنا ایجاد نمی‌کنند. در عوض 0 برمی‌گردانند.

counter['d']
0

می‌توانیم از شیءهای Counter برای حل یکی از تمرین‌های فصل ۱۰ استفاده کنیم، که تابعی می‌خواهد که دو کلمه بگیرد و بررسی کند که آیا هم‌آراینده (anagram) هستند – یعنی حرف‌های یکی را می‌توان دوباره چید و دیگری را هجی کرد.

این راه‌حلی است که شیءهای Counter به کار می‌برد.

def is_anagram(word1, word2):
    return Counter(word1) == Counter(word2)

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

Counter متدی به نام most_common دارد که فهرستی از جفت‌های مقدار-بسامد برمی‌گرداند، از رایج‌ترین به کم‌رایج‌ترین مرتب‌شده.

counter.most_common()
[('a', 3), ('n', 2), ('b', 1)]

همچنین متدها و عملگرهایی برای انجام عملیات شبیه مجموعه دارند، از جمله جمع، تفریق، اجتماع و اشتراک. برای مثال، عملگر + دو شیء Counter را ترکیب می‌کند و Counter جدیدی می‌سازد که کلیدهای هر دو و مجموع شمارش‌ها را دارد.

می‌توانیم آن را با ساختن یک Counter از حرف‌های 'bans' و جمع کردن آن با حرف‌های 'banana' آزمایش کنیم.

counter2 = Counter('bans')
counter + counter2
Counter({'a': 4, 'n': 3, 'b': 2, 's': 1})

در تمرین‌های پایان این فصل فرصت خواهید داشت عملیات دیگر Counter را بررسی کنید.

۱۸.۳. defaultdict

ماژول collections همچنین defaultdict را دارد، که شبیه دیکشنری است با این تفاوت که اگر به کلیدی که وجود ندارد دسترسی پیدا کنید، خودکار مقدار جدیدی تولید می‌کند.

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

برای مثال، این یک defaultdict است که هر وقت لازم شود یک list جدید می‌سازد.

from collections import defaultdict

d = defaultdict(list)
d
defaultdict(<class 'list'>, {})

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

t = d['new key']
t
[]

فهرست جدید، که اسمش را t گذاشتیم، به دیکشنری هم اضافه می‌شود. پس اگر t را تغییر دهیم، تغییر در d ظاهر می‌شود:

t.append('new value')
d['new key']
['new value']

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

در یکی از تمرین‌های فصل ۱۱، دیکشنری‌ای ساختم که از یک رشته‌ی مرتب‌شده‌ی حروف به فهرست کلمه‌هایی که با آن حروف قابل هجی‌کردن‌اند نگاشت می‌کند. برای مثال، رشته‌ی 'opst' به فهرست ['opts', 'post', 'pots', 'spot', 'stop', 'tops'] نگاشت می‌شود. این هم کد اصلی.

def all_anagrams(filename):
    d = {}
    for line in open(filename):
        word = line.strip().lower()
        t = signature(word)
        if t not in d:
            d[t] = [word]
        else:
            d[t].append(word)
    return d

و این نسخه‌ی ساده‌تر با defaultdict است.

def all_anagrams(filename):
    d = defaultdict(list)
    for line in open(filename):
        word = line.strip().lower()
        t = signature(word)
        d[t].append(word)
    return d

در تمرین‌های پایان فصل فرصت خواهید داشت با شیءهای defaultdict تمرین کنید.

from collections import defaultdict

d = defaultdict(list)
key = ('out', 'of')
d[key].append('sight')
d[key]
['sight']

۱۸.۴. عبارت‌های شرطی

دستورهای شرطی اغلب برای انتخاب یکی از دو مقدار به کار می‌روند، مثل این:

import math
x = 5
if x > 0:
    y = math.log(x)
else:
    y = float('nan')
y
1.6094379124341003

این دستور بررسی می‌کند که x مثبت است یا نه. اگر باشد، لگاریتمش را حساب می‌کند. اگر نباشد، math.log یک ValueError ایجاد می‌کند. برای اینکه برنامه متوقف نشود، یک NaN تولید می‌کنیم، که مقدار ویژه‌ی ممیز شناوری است و «عدد نیست» (Not a Number) را نشان می‌دهد.

می‌توانیم این دستور را با عبارت شرطی (conditional expression) مختصرتر بنویسیم.

y = math.log(x) if x > 0 else float('nan')
y
1.6094379124341003

این خط را تقریباً مثل انگلیسی می‌توانید بخوانید: «y برابر log-x می‌شود اگر x بزرگ‌تر از ۰ باشد؛ وگرنه NaN می‌شود».

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

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)

و این نسخه‌ای با عبارت شرطی است.

def factorial(n):
    return 1 if n == 0 else n * factorial(n-1)

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

class Kangaroo:
    def __init__(self, name, contents=None):
        self.name = name
        if contents is None:
            contents = []
        self.contents = contents

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

def __init__(self, name, contents=None):
    self.name = name
    self.contents = [] if contents is None else contents 

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

۱۸.۵. شمول فهرست (List comprehensions)

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

title = 'once upon a time in a small town'

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

t = []
for word in title.split():
    t.append(word.capitalize())

' '.join(t)
'Once Upon A Time In A Small Town'

می‌توانیم همین کار را با شمول فهرست (list comprehension) مختصرتر انجام دهیم:

t = [word.capitalize() for word in title.split()]

' '.join(t)
'Once Upon A Time In A Small Town'

عملگر کروشه نشان می‌دهد که فهرست جدیدی می‌سازیم. عبارت داخل کروشه عنصرهای فهرست را مشخص می‌کند و بند for نشان می‌دهد که روی چه دنباله‌ای حلقه می‌زنیم.

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

به‌عنوان مثال دیگر، در فصل ۹ از این حلقه برای خواندن کلمه‌ها از یک فایل و افزودنشان به فهرست استفاده کردیم.

download('https://raw.githubusercontent.com/AllenDowney/ThinkPython2/master/code/words.txt');
word_list = []

for line in open('words.txt'):
    word = line.strip()
    word_list.append(word)
len(word_list)
113783

این‌طور می‌توانیم آن را به‌صورت شمول فهرست بنویسیم.

word_list = [line.strip() for line in open('words.txt')]
len(word_list)
113783

شمول فهرست می‌تواند بند if هم داشته باشد که تعیین می‌کند کدام عنصرها در فهرست قرار بگیرند. برای مثال، این حلقه for است که در فصل ۱۰ برای ساختن فهرستی فقط از کلمه‌های word_list که palindrome (کلمه‌ی متقارن) هستند به کار بردیم.

def is_palindrome(word):
    return list(reversed(word)) == list(word)
palindromes = []

for word in word_list:
    if is_palindrome(word):
        palindromes.append(word)
palindromes[:10]
['aa', 'aba', 'aga', 'aha', 'ala', 'alula', 'ama', 'ana', 'anna', 'ava']

این‌طور می‌توانیم همین کار را با شمول فهرست انجام دهیم.

palindromes = [word for word in word_list if is_palindrome(word)]
palindromes[:10]
['aa', 'aba', 'aga', 'aha', 'ala', 'alula', 'ama', 'ana', 'anna', 'ava']

وقتی شمول فهرست به‌عنوان آرگومان یک تابع به کار می‌رود، اغلب می‌توانیم کروشه‌ها را حذف کنیم. برای مثال، فرض کنید می‌خواهیم $1 / 2^n$ را برای مقدارهای $n$ از ۰ تا ۹ جمع کنیم. می‌توانیم از شمول فهرست این‌طور استفاده کنیم.

sum([1/2**n for n in range(10)])
1.998046875

یا می‌توانیم کروشه‌ها را این‌طور کنار بگذاریم.

sum(1/2**n for n in range(10))
1.998046875

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

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

اما به دفاع از خودم، شمول فهرست اشکال‌زدایی‌اش سخت‌تر است چون نمی‌توانید داخل حلقه دستور print بگذارید. پیشنهاد می‌کنم فقط وقتی از آن‌ها استفاده کنید که محاسبه آن‌قدر ساده باشد که احتمالاً بار اول درست می‌نویسیدش. یا فکر کنید یک حلقه‌ی for بنویسید و اشکال‌زدایی کنید و بعد آن را به شمول فهرست تبدیل کنید.

۱۸.۶. any و all

پایتون تابع داخلی any را دارد که دنباله‌ای از مقدارهای بولی می‌گیرد و اگر هر یک از مقدارها True باشد True برمی‌گرداند.

any([False, False, True])
True

any اغلب با عبارت‌های مولد به کار می‌رود.

any(letter == 't' for letter in 'mount')
True

این مثال خیلی مفید نیست چون همان کاری را می‌کند که عملگر in می‌کند. اما می‌توانیم از any برای نوشتن راه‌حل‌های مختصر بعضی تمرین‌های فصل ۷ استفاده کنیم. برای مثال، می‌توانیم uses_none را این‌طور بنویسیم.

def uses_none(word, forbidden):
    """Checks whether a word avoids forbidden letters."""
    return not any(letter in forbidden for letter in word)
uses_none('banana', 'xyz')
True
uses_none('apple', 'efg')
False

این تابع حرف‌های word را پیمایش می‌کند و بررسی می‌کند که آیا هیچ‌کدام در forbidden هستند. استفاده از any با عبارت مولد کارآمد است چون اگر مقدار True پیدا کند بلافاصله متوقف می‌شود، پس لازم نیست کل دنباله را پیمایش کند.

پایتون تابع داخلی دیگری هم دارد، all، که اگر هر عنصر دنباله True باشد True برمی‌گرداند. می‌توانیم از آن برای نوشتن نسخه‌ای مختصر از uses_all استفاده کنیم.

def uses_all(word, required):
    """Check whether a word uses all required letters."""
    return all(letter in word for letter in required)
uses_all('banana', 'ban')
True
uses_all('apple', 'api')
False

عبارت‌هایی که از any و all استفاده می‌کنند می‌توانند مختصر، کارآمد و خواندنی باشند.

۱۸.۷. چندتایی‌های نام‌دار (Named tuples)

ماژول collections تابعی به نام namedtuple دارد که می‌توان با آن کلاس‌های ساده ساخت. برای مثال، شیء Point در فصل ۱۶ فقط دو صفت دارد، x و y. این‌طور آن را تعریف کردیم.

class Point:
    """Represents a point in 2-D space."""
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def __str__(self):
        return f'({self.x}, {self.y})'

این کد زیادی است برای رساندن مقدار کمی اطلاعات. namedtuple راه مختصرتری برای تعریف کلاس‌هایی مثل این فراهم می‌کند.

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])

آرگومان اول نام کلاسی است که می‌خواهید بسازید. دومی فهرست صفت‌هایی است که شیءهای Point باید داشته باشند. نتیجه یک شیء کلاس است، به همین دلیل به متغیری با نام بزرگ‌حرف نسبت داده می‌شود.

کلاسی که با namedtuple ساخته شود، متد __init__ دارد که مقدارها را به صفت‌ها نسبت می‌دهد و __str__ (در عمل __repr__) که شیء را به شکل خواناپذیر نمایش می‌دهد. پس می‌توانیم یک شیء Point را این‌طور بسازیم و نمایش دهیم.

p = Point(1, 2)
p
Point(x=1, y=2)

Point همچنین متد __eq__ دارد که بررسی می‌کند دو شیء Point هم‌ارزند – یعنی صفت‌هایشان یکسان است.

p == Point(1, 2)
True

می‌توانید به عنصرهای یک چندتایی نام‌دار با نام یا با اندیس دسترسی پیدا کنید.

p.x, p.y
(1, 2)
p[0], p[1]
(1, 2)

همچنین می‌توانید یک چندتایی نام‌دار را مثل چندتایی معمولی به کار ببرید، مثل این انتساب.

x, y = p
x, y
(1, 2)

اما شیءهای namedtuple تغییرناپذیرند. بعد از مقداردهی اولیه‌ی صفت‌ها، نمی‌توان آن‌ها را تغییر داد.

%%expect TypeError

p[0] = 3
TypeError: 'Point' object does not support item assignment
%%expect AttributeError

p.x = 3
AttributeError: can't set attribute

namedtuple راهی سریع برای تعریف کلاس‌های ساده است. عیبش این است که کلاس‌های ساده همیشه ساده نمی‌مانند. ممکن است بعداً بخواهید متدهایی به چندتایی نام‌دار اضافه کنید. در آن صورت، می‌توانید کلاس جدیدی تعریف کنید که از چندتایی نام‌دار ارث ببرد.

class Pointier(Point):
    """This class inherits from Point"""

یا در آن مرحله می‌توانید به تعریف کلاس معمولی برگردید.

۱۸.۸. بسته‌بندی آرگومان‌های کلیدواژه‌ای

در فصل ۱۱ تابعی نوشتیم که آرگومان‌هایش را در یک چندتایی بسته‌بندی (pack) می‌کند.

def mean(*args):
    return sum(args) / len(args)

می‌توانید این تابع را با هر تعداد آرگومان فراخوانی کنید.

mean(1, 2, 3)
2.0

اما عملگر * آرگومان‌های کلیدواژه‌ای را بسته‌بندی نمی‌کند. پس فراخوانی این تابع با آرگومان کلیدواژه‌ای باعث خطا می‌شود.

%%expect TypeError

mean(1, 2, start=3)
TypeError: mean() got an unexpected keyword argument 'start'

برای بسته‌بندی آرگومان‌های کلیدواژه‌ای، می‌توانیم از عملگر ** استفاده کنیم:

def mean(*args, **kwargs):
    print(kwargs)
    return sum(args) / len(args)

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

mean(1, 2, start=3)
{'start': 3}
1.5

در این مثال، مقدار kwargs چاپ می‌شود، اما در غیر این صورت اثری ندارد.

اما عملگر ** را می‌توان در فهرست آرگومان‌ها هم برای باز کردن (unpack) دیکشنری به کار برد. برای مثال، این نسخه‌ای از mean است که هر آرگومان کلیدواژه‌ای را که بگیرد بسته‌بندی می‌کند و بعد به‌صورت آرگومان‌های کلیدواژه‌ای برای sum بازشان می‌کند.

def mean(*args, **kwargs):
    return sum(args, **kwargs) / len(args)

حالا اگر mean را با start به‌عنوان آرگومان کلیدواژه‌ای فراخوانی کنیم، به sum پاس داده می‌شود و sum از آن به‌عنوان نقطه‌ی شروع جمع استفاده می‌کند. در مثال زیر start=3 پیش از محاسبه‌ی میانگین، 3 را به مجموع اضافه می‌کند، پس مجموع 6 است و نتیجه 3 می‌شود.

mean(1, 2, start=3)
3.0

به‌عنوان مثال دیگر، اگر دیکشنری‌ای با کلیدهای x و y داشته باشیم، می‌توانیم با عملگر باز کردن از آن برای ساختن شیء Point استفاده کنیم.

d = dict(x=1, y=2)
Point(**d)
Point(x=1, y=2)

بدون عملگر باز کردن، d به‌عنوان یک آرگومان موقعیتی در نظر گرفته می‌شود، پس به x نسبت داده می‌شود و TypeError می‌گیریم چون آرگومان دومی برای نسبت دادن به y نیست.

%%expect TypeError

d = dict(x=1, y=2)
Point(d)
TypeError: Point.__new__() missing 1 required positional argument: 'y'

وقتی با توابعی کار می‌کنید که تعداد زیادی آرگومان کلیدواژه‌ای دارند، اغلب مفید است دیکشنری‌هایی بسازید و دست‌به‌دست کنید که گزینه‌های پرکاربرد را مشخص می‌کنند.

def pack_and_print(**kwargs):
    print(kwargs)
    
pack_and_print(a=1, b=2)
{'a': 1, 'b': 2}

۱۸.۹. اشکال‌زدایی

در فصل‌های قبل از doctest برای آزمودن توابع استفاده کردیم. برای مثال، این تابعی به نام add است که دو عدد می‌گیرد و مجموعشان را برمی‌گرداند. یک doctest دارد که بررسی می‌کند 2 + 2 برابر 4 است.

def add(a, b):
    '''Add two numbers.
    
    >>> add(2, 2)
    4
    '''
    return a + b

این تابع یک شیء تابع می‌گیرد و doctestهایش را اجرا می‌کند.

from doctest import run_docstring_examples

def run_doctests(func):
    run_docstring_examples(func, globals(), name=func.__name__)

پس می‌توانیم add را این‌طور آزمون کنیم.

run_doctests(add)

خروجی ندارد، که یعنی همه‌ی آزمون‌ها گذشتند.

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

from unittest import TestCase

class TestExample(TestCase):

    def test_add(self):
        result = add(2, 2)
        self.assertEqual(result, 4)

اول TestCase را وارد می‌کنیم، که کلاسی در ماژول unittest است. برای استفاده از آن، باید کلاس جدیدی تعریف کنیم که از TestCase ارث ببرد و دست‌کم یک متد آزمون داشته باشد. نام متد آزمون باید با test شروع شود و باید نشان دهد کدام تابع را آزمون می‌کند.

در این مثال، test_add تابع add را با فراخوانی آن، ذخیره‌ی نتیجه و احضار assertEqual که از TestCase به ارث رسیده آزمون می‌کند. assertEqual دو آرگومان می‌گیرد و بررسی می‌کند که برابرند.

برای اجرای این متد آزمون، باید تابعی در unittest به نام main را اجرا کنیم و چند آرگومان کلیدواژه‌ای به آن بدهیم. تابع زیر جزئیات را نشان می‌دهد – اگر کنجکاو هستید، می‌توانید از یک دستیار مجازی بخواهید توضیح دهد چطور کار می‌کند.

import unittest

def run_unittest():
    unittest.main(argv=[''], verbosity=0, exit=False)

run_unittest کلاس TestExample را به‌عنوان آرگومان نمی‌گیرد – در عوض، دنبال کلاس‌هایی که از TestCase ارث می‌برند می‌گردد. بعد دنبال متدهایی که با test شروع می‌شوند می‌گردد و آن‌ها را اجرا می‌کند. این فرایند کشف آزمون (test discovery) نامیده می‌شود.

این چیزی است که وقتی run_unittest را فراخوانی می‌کنیم رخ می‌دهد.

run_unittest()
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

unittest.main تعداد آزمون‌هایی را که اجرا کرده و نتایج را گزارش می‌کند. در این مورد OK نشان می‌دهد که آزمون‌ها گذشتند.

برای دیدن اینکه وقتی آزمونی شکست می‌خورد چه می‌شود، متد آزمون نادرستی به TestExample اضافه می‌کنیم.

%%add_method_to TestExample

    def test_add_broken(self):
        result = add(2, 2)
        self.assertEqual(result, 100)

این چیزی است که وقتی آزمون‌ها را اجرا می‌کنیم رخ می‌دهد.

run_unittest()
======================================================================
FAIL: test_add_broken (__main__.TestExample.test_add_broken)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "...", line 20, in test_add_broken
    self.assertEqual(result, 100)
AssertionError: 4 != 100

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

(مسیر فایل و شماره‌ی خط در اجرای شما فرق می‌کند.)

گزارش شامل متد آزمونی است که شکست خورد و یک پیام خطا که نشان می‌دهد کجا. خلاصه نشان می‌دهد که دو آزمون اجرا شد و یکی شکست خورد.

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

۱۸.۱۰. واژه‌نامه

کارخانه (factory): تابعی که برای ساختن شیءها به کار می‌رود، که اغلب به‌عنوان پارامتر به تابع دیگری داده می‌شود.

عبارت شرطی (conditional expression): عبارتی که از یک شرط برای انتخاب یکی از دو مقدار استفاده می‌کند.

شمول فهرست (list comprehension): راهی مختصر برای پیمایش یک دنباله و ساختن فهرست.

عبارت مولد (generator expression): شبیه شمول فهرست، با این تفاوت که فهرست نمی‌سازد.

کشف آزمون (test discovery): فرایندی برای یافتن و اجرای آزمون‌ها.

۱۸.۱۱. تمرین‌ها

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

%xmode Verbose

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

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

  • “What are the methods and operators of Python’s set class?” (متدها و عملگرهای کلاس set پایتون چیست؟)

  • “What are the methods and operators of Python’s Counter class?” (متدها و عملگرهای کلاس Counter پایتون چیست؟)

  • “What is the difference between a Python list comprehension and a generator expression?” (تفاوت شمول فهرست و عبارت مولد در پایتون چیست؟)

  • “When should I use Python’s namedtuple rather than define a new class?” (چه وقت باید به‌جای تعریف کلاس جدید از namedtuple پایتون استفاده کنم؟)

  • “What are some uses of packing and unpacking keyword arguments?” (بسته‌بندی و بازکردن آرگومان‌های کلیدواژه‌ای چه کاربردهایی دارد؟)

  • “How does unittest do test discovery?” (unittest چطور کشف آزمون را انجام می‌دهد؟)

  • “Along with assertEqual, what are the most commonly used methods in unittest.TestCase?” (علاوه بر assertEqual، رایج‌ترین متدهای unittest.TestCase کدام‌اند؟)

  • “What are the pros and cons of doctest and unittest?” (مزایا و معایب doctest و unittest چیست؟)

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

تمرین ۱

یکی از تمرین‌های فصل ۷ تابعی به نام uses_none می‌خواهد که کلمه‌ای و رشته‌ای از حرف‌های ممنوع بگیرد و اگر کلمه از هیچ‌کدام از حرف‌ها استفاده نکند True برگرداند. این یک راه‌حل است.

def uses_none(word, forbidden):
    for letter in word.lower():
        if letter in forbidden.lower():
            return False
    return True

نسخه‌ای از این تابع بنویسید که به‌جای حلقه‌ی for از عملیات set استفاده کند. راهنمایی: از یک دستیار مجازی بپرسید: “How do I compute the intersection of Python sets?” (چطور اشتراک مجموعه‌های پایتون را حساب کنم؟)

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

def uses_none(word, forbidden):
    """Checks whether a word avoid forbidden letters.
    
    >>> uses_none('banana', 'xyz')
    True
    >>> uses_none('apple', 'efg')
    False
    >>> uses_none('', 'abc')
    True
    """
    return False
# Solution goes here
from doctest import run_docstring_examples

def run_doctests(func):
    run_docstring_examples(func, globals(), name=func.__name__)
run_doctests(uses_none)

تمرین ۲

Scrabble بازی تخته‌ای است که هدفش استفاده از کاشی‌های حرف برای هجی کردن کلمه‌هاست. برای مثال، اگر کاشی‌هایی با حرف‌های T، A، B، L، E داشته باشیم، می‌توانیم BELT و LATE را با زیرمجموعه‌ای از کاشی‌ها هجی کنیم – اما نمی‌توانیم BEET را هجی کنیم چون دو E نداریم.

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

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

def can_spell(letters, word):
    """Check whether the letters can spell the word.
    
    >>> can_spell('table', 'belt')
    True
    >>> can_spell('table', 'late')
    True
    >>> can_spell('table', 'beet')
    False
    """
    return False
# Solution goes here
run_doctests(can_spell)

تمرین ۳

در یکی از تمرین‌های فصل ۱۷، راه‌حل من برای has_straightflush از متد زیر استفاده می‌کند، که یک PokerHand را به فهرستی از چهار دست افراز می‌کند که هر دست ورق‌های یک خال را دارد.

    def partition(self):
        """Make a list of four hands, each containing only one suit."""
        hands = []
        for i in range(4):
            hands.append(PokerHand())
            
        for card in self.cards:
            hands[card.suit].add_card(card)
            
        return hands

نسخه‌ی ساده‌شده‌ی این تابع را با defaultdict بنویسید.

این طرح اولیه‌ی کلاس PokerHand است که می‌توانید برای شروع استفاده کنید.

class PokerHand(Hand):
    
    def partition(self):
        return {}
# Solution goes here

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

cards = Deck.make_cards()
deck = Deck(cards)
deck.shuffle()

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

random_hand = PokerHand('random')

for i in range(7):
    card = deck.pop_card()
    random_hand.add_card(card)
    
print(random_hand)

(خروجی به بُر خوردن تصادفی دسته بستگی دارد و هر بار فرق می‌کند: هفت ورق تصادفی.)

اگر partition را احضار کنید و نتیجه را چاپ کنید، هر دست باید فقط ورق‌های یک خال را داشته باشد.

hand_dict = random_hand.partition()

for hand in hand_dict.values():
    print(hand)
    print()

تمرین ۴

این تابع فصل ۱۱ است که اعداد فیبوناچی را حساب می‌کند.

def fibonacci(n):
    if n == 0:
        return 0
    
    if n == 1:
        return 1

    return fibonacci(n-1) + fibonacci(n-2)

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

# Solution goes here
fibonacci(10)    # should be 55
fibonacci(20)    # should be 6765

تمرین ۵

این تابعی است که ضریب دوجمله‌ای را به‌صورت بازگشتی حساب می‌کند.

def binomial_coeff(n, k):
    """Compute the binomial coefficient "n choose k".

    n: number of trials
    k: number of successes

    returns: int
    """
    if k == 0:
        return 1
    
    if n == 0:
        return 0

    return binomial_coeff(n-1, k) + binomial_coeff(n-1, k-1)

بدنه‌ی تابع را با عبارت‌های شرطی تودرتو بازنویسی کنید.

این تابع خیلی کارآمد نیست چون در نهایت مقدارهای یکسان را بارها و بارها حساب می‌کند. با یادداشت‌برداری (memoization)، همان‌طور که در فصل ۱۰ توضیح داده شد، آن را کارآمدتر کنید.

# Solution goes here
binomial_coeff(10, 4)    # should be 210

تمرین ۶

این متد __str__ از کلاس Deck در فصل ۱۷ است.

%%add_method_to Deck

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

نسخه‌ی مختصرتری از این متد با شمول فهرست یا عبارت مولد بنویسید.

# Solution goes here

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

cards = Deck.make_cards()
deck = Deck(cards)
print(deck)
2 of Clubs
3 of Clubs
4 of Clubs
5 of Clubs
6 of Clubs
7 of Clubs
8 of Clubs
9 of Clubs
10 of Clubs
Jack of Clubs
Queen of Clubs
King of Clubs
Ace of Clubs
2 of Diamonds
...
King of Spades
Ace of Spades

(خروجی کامل ۵۲ خط دارد؛ برای کوتاهی فقط ابتدا و انتهای آن را آورده‌ام.)