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

چندتایی‌ها (فصل ۱۱ Think Python)

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

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

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

سه نکته درباره‌ی این ترجمه: (۱) در کتاب اصلی، نشانی ایمیل نمونه‌ی مثال بخش «انتساب چندتایی» به یک گروه طنز اشاره دارد؛ من آن را با یک نشانی ساده جایگزین کرده‌ام. (۲) در آخرین تمرین (تمرین امتیازی)، کتاب بخشی از یک معمای رادیویی (Car Talk) را عیناً نقل می‌کند؛ آن متن را بازنقل نکرده‌ام و به‌جای آن مسئله را با کلمه‌های خودم خلاصه کرده‌ام. (۳) خروجی‌های نمایش‌داده‌شده‌ی زیر کدها را با اجرای همین کدها به دست آورده‌ام.

در این فصل یک نوع داخلی دیگر، یعنی چندتایی (tuple)، معرفی می‌شود و بعد نشان می‌دهد فهرست‌ها، دیکشنری‌ها و چندتایی‌ها چطور با هم کار می‌کنند. همچنین انتساب چندتایی (tuple assignment) و یک ویژگی مفید برای توابعی با فهرست آرگومان متغیر را معرفی می‌کند: عملگرهای بسته‌بندی و بازکردن بسته (packing و unpacking).

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

یک نکته: دو شیوه برای تلفظ «tuple» وجود دارد. بعضی‌ها می‌گویند «تاپل» که با «supple» هم‌قافیه است. اما در زمینه‌ی برنامه‌نویسی بیشتر مردم می‌گویند «توپل» که با «quadruple» هم‌قافیه است.

۱۱.۱. چندتایی‌ها شبیه فهرست‌اند

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

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

t = 'l', 'u', 'p', 'i', 'n'
type(t)
tuple

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

t = ('l', 'u', 'p', 'i', 'n')
type(t)
tuple

برای ساختن چندتایی با یک عنصر، باید یک ویرگول در انتها بگذارید.

t1 = 'p',
type(t1)
tuple

یک مقدار تنها در پرانتز چندتایی نیست.

t2 = ('p')
type(t2)
str

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

t = tuple()
t
()

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

t = tuple('lupin')
t
('l', 'u', 'p', 'i', 'n')

چون tuple نام یک تابع داخلی است، باید از استفاده از آن به‌عنوان نام متغیر پرهیز کنید.

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

t[0]
'l'

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

t[1:3]
('u', 'p')

عملگر + چندتایی‌ها را الحاق می‌کند.

tuple('lup') + ('i', 'n')
('l', 'u', 'p', 'i', 'n')

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

tuple('spam') * 2 
('s', 'p', 'a', 'm', 's', 'p', 'a', 'm')

تابع sorted با چندتایی‌ها کار می‌کند – اما نتیجه یک فهرست است، نه چندتایی.

sorted(t)
['i', 'l', 'n', 'p', 'u']

تابع reversed هم با چندتایی‌ها کار می‌کند.

reversed(t)
<reversed at 0x7f086efbac80>

نتیجه یک شیء reversed است که می‌توانیم آن را به فهرست یا چندتایی تبدیل کنیم. (آدرس حافظه در خروجی در هر اجرا فرق می‌کند.)

tuple(reversed(t))
('n', 'i', 'p', 'u', 'l')

بر اساس مثال‌های تا اینجا، شاید به نظر برسد چندتایی‌ها همان فهرست‌ها هستند.

۱۱.۲. اما چندتایی‌ها تغییرناپذیرند

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

t[0] = 'L'
TypeError: 'tuple' object does not support item assignment

و چندتایی‌ها هیچ‌یک از متدهایی را که فهرست‌ها را تغییر می‌دهند، مثل append و remove، ندارند.

t.remove('l')
AttributeError: 'tuple' object has no attribute 'remove'

به یاد بیاورید که «صفت» (attribute) متغیر یا متدی است که به یک شیء وابسته است – این پیام خطا یعنی چندتایی‌ها متدی به نام remove ندارند.

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

d = {}
d[1, 2] = 3
d[3, 4] = 7

می‌توانیم یک چندتایی را در دیکشنری این‌طور جست‌وجو کنیم:

d[1, 2]
3

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

t = (3, 4)
d[t]
7

چندتایی‌ها می‌توانند به‌عنوان مقدار در دیکشنری هم ظاهر شوند.

t = tuple('abc')
d = {'key': t}
d
{'key': ('a', 'b', 'c')}

۱۱.۳. انتساب چندتایی

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

a, b = 1, 2

مقدارها از چپ به راست به متغیرها نسبت داده می‌شوند – در این مثال a مقدار 1 و b مقدار 2 را می‌گیرد. می‌توانیم نتیجه را این‌طور نمایش دهیم:

a, b
(1, 2)

به‌طور کلی‌تر، اگر سمت چپ نسبت‌دهی چندتایی باشد، سمت راست می‌تواند هر نوع دنباله‌ای – رشته، فهرست یا چندتایی – باشد. برای مثال، برای شکستن یک نشانی ایمیل به نام کاربری و دامنه، می‌توانید بنویسید:

email = 'student@example.org'
username, domain = email.split('@')

مقدار بازگشتی split فهرستی با دو عنصر است – عنصر اول به username و دومی به domain نسبت داده می‌شود.

username, domain
('student', 'example.org')

تعداد متغیرهای سمت چپ و تعداد مقدارهای سمت راست باید برابر باشد – در غیر این صورت ValueError می‌گیرید.

a, b = 1, 2, 3
ValueError: too many values to unpack (expected 2)

انتساب چندتایی وقتی می‌خواهید مقدار دو متغیر را جابه‌جا کنید مفید است. با نسبت‌دهی‌های معمول باید از یک متغیر موقت استفاده کنید، مثل این:

temp = a
a = b
b = temp

این کار می‌کند، اما با انتساب چندتایی می‌توانیم همین کار را بدون متغیر موقت انجام دهیم.

a, b = b, a

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

می‌توانیم از انتساب چندتایی در دستور for هم استفاده کنیم. برای مثال، برای پیمایش موردهای یک دیکشنری، می‌توانیم از متد items استفاده کنیم.

d = {'one': 1, 'two': 2}

for item in d.items():
    key, value = item
    print(key, '->', value)
one -> 1
two -> 2

هر بار که حلقه تکرار می‌شود، به item یک چندتایی شامل یک کلید و مقدار متناظرش نسبت داده می‌شود.

می‌توانیم این حلقه را مختصرتر این‌طور بنویسیم:

for key, value in d.items():
    print(key, '->', value)
one -> 1
two -> 2

هر بار که حلقه تکرار می‌شود، یک کلید و مقدار متناظرش مستقیماً به key و value نسبت داده می‌شوند.

۱۱.۴. چندتایی‌ها به‌عنوان مقدار بازگشتی

به‌طور دقیق، یک تابع فقط می‌تواند یک مقدار برگرداند، اما اگر مقدار یک چندتایی باشد، اثرش مثل برگرداندن چند مقدار است. برای مثال، اگر بخواهید دو عدد صحیح را تقسیم کنید و خارج‌قسمت و باقی‌مانده را حساب کنید، ناکارآمد است که اول x//y و بعد x%y را حساب کنید. بهتر است هر دو را همزمان حساب کنید.

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

divmod(7, 3)
(2, 1)

می‌توانیم از انتساب چندتایی برای ذخیره‌ی عنصرهای چندتایی در دو متغیر استفاده کنیم.

quotient, remainder = divmod(7, 3)
quotient
2
remainder
1

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

def min_max(t):
    return min(t), max(t)

max و min توابع داخلی‌ای هستند که بزرگ‌ترین و کوچک‌ترین عنصر یک دنباله را پیدا می‌کنند. min_max هر دو را حساب می‌کند و چندتایی‌ای از دو مقدار برمی‌گرداند.

min_max([2, 4, 1, 3])
(1, 4)

می‌توانیم نتیجه‌ها را این‌طور به متغیرها نسبت دهیم:

low, high = min_max([2, 4, 1, 3])
low, high
(1, 4)

۱۱.۵. بسته‌بندی آرگومان‌ها (Argument packing)

توابع می‌توانند تعداد متغیری آرگومان بگیرند. نام پارامتری که با عملگر * شروع شود، آرگومان‌ها را در یک چندتایی بسته‌بندی (pack) می‌کند. برای مثال، تابع زیر هر تعداد آرگومان می‌گیرد و میانگین حسابی آن‌ها را حساب می‌کند – یعنی مجموعشان تقسیم بر تعداد آرگومان‌ها.

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

پارامتر می‌تواند هر نامی که دوست دارید داشته باشد، اما args قراردادی است. می‌توانیم تابع را این‌طور فراخوانی کنیم:

mean(1, 2, 3)
2.0

اگر دنباله‌ای از مقدارها دارید و می‌خواهید آن‌ها را به‌صورت چند آرگومان به یک تابع بدهید، می‌توانید از عملگر * برای باز کردن بسته (unpack) چندتایی استفاده کنید. برای مثال، divmod دقیقاً دو آرگومان می‌گیرد – اگر یک چندتایی را به‌عنوان پارامتر بدهید، خطا می‌گیرید.

t = (7, 3)
divmod(t)
TypeError: divmod expected 2 arguments, got 1

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

divmod(*t)
(2, 1)

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

def trimmed_mean(*args):
    low, high = min_max(args)
    trimmed = list(args)
    trimmed.remove(low)
    trimmed.remove(high)
    return mean(*trimmed)

اول با min_max کوچک‌ترین و بزرگ‌ترین عنصر را پیدا می‌کند. بعد args را به فهرست تبدیل می‌کند تا بتواند از متد remove استفاده کند. در پایان فهرست را باز می‌کند تا عنصرها به‌جای یک فهرست واحد، به‌صورت آرگومان‌های جداگانه به mean داده شوند.

این مثالی است که اثرش را نشان می‌دهد.

mean(1, 2, 3, 10)
4.0
trimmed_mean(1, 2, 3, 10)
2.5

این نوع میانگین «پیراسته» (trimmed) در بعضی ورزش‌ها با داوری ذهنی – مثل شیرجه و ژیمناستیک – استفاده می‌شود تا اثر داوری که نمره‌اش با بقیه فرق دارد کم شود.

۱۱.۶. Zip

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

scores1 = [1, 2, 4, 5, 1, 5, 2]
scores2 = [5, 5, 2, 2, 5, 2, 3]

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

zip(scores1, scores2)
<zip at 0x7f086efd4040>

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

for pair in zip(scores1, scores2):
     print(pair)
(1, 5)
(2, 5)
(4, 2)
(5, 2)
(1, 5)
(5, 2)
(2, 3)

هر بار که حلقه تکرار می‌شود، به pair یک چندتایی از امتیازها نسبت داده می‌شود. پس می‌توانیم امتیازها را به متغیرها نسبت دهیم و پیروزی‌های تیم اول را این‌طور بشماریم:

wins = 0
for team1, team2 in zip(scores1, scores2):
    if team1 > team2:
        wins += 1
        
wins
3

متأسفانه تیم اول فقط سه بازی را برد و سری را باخت.

اگر دو فهرست دارید و فهرستی از جفت‌ها می‌خواهید، می‌توانید از zip و list استفاده کنید.

t = list(zip(scores1, scores2))
t
[(1, 5), (2, 5), (4, 2), (5, 2), (1, 5), (5, 2), (2, 3)]

نتیجه فهرستی از چندتایی‌هاست، پس می‌توانیم نتیجه‌ی آخرین بازی را این‌طور بگیریم:

t[-1]
(2, 3)

اگر فهرستی از کلیدها و فهرستی از مقدارها دارید، می‌توانید با zip و dict یک دیکشنری بسازید. برای مثال، این‌طور می‌توانیم دیکشنری‌ای بسازیم که هر حرف را به جایگاهش در الفبا نگاشت می‌کند.

letters = 'abcdefghijklmnopqrstuvwxyz'
numbers = range(len(letters))
letter_map = dict(zip(letters, numbers))

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

letter_map['a'], letter_map['z']
(0, 25)

در این نگاشت، اندیس 'a' برابر 0 و اندیس 'z' برابر 25 است.

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

enumerate('abc')
<enumerate at 0x7f086ef77c90>

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

for index, element in enumerate('abc'):
    print(index, element)
0 a
1 b
2 c

۱۱.۷. مقایسه و مرتب‌سازی

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

(0, 1, 2) < (0, 3, 4)
True

عنصرهای بعدی در نظر گرفته نمی‌شوند – حتی اگر خیلی بزرگ باشند.

(0, 1, 2000000) < (0, 3, 4)
True

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

def value_counts(string):
    counter = {}
    for letter in string:
        if letter not in counter:
            counter[letter] = 1
        else:
            counter[letter] += 1
    return counter

این نتیجه‌ی رشته‌ی 'banana' است.

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

با فقط سه مورد، به‌راحتی می‌بینیم که پرتکرارترین حرف 'a' است که سه بار ظاهر می‌شود. اما اگر موردهای بیشتری بود، مفید می‌بود که آن‌ها را خودکار مرتب کنیم.

می‌توانیم موردها را این‌طور از counter بگیریم.

items = counter.items()
items
dict_items([('b', 1), ('a', 3), ('n', 2)])

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

sorted(items)
[('a', 3), ('b', 1), ('n', 2)]

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

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

def second_element(t):
    return t[1]

بعد می‌توانیم آن تابع را به‌عنوان آرگومان اختیاری به نام key به sorted بدهیم، که نشان می‌دهد این تابع باید برای محاسبه‌ی کلید مرتب‌سازی (sort key) هر مورد به کار رود.

sorted_items = sorted(items, key=second_element)
sorted_items
[('b', 1), ('n', 2), ('a', 3)]

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

sorted_items[-1]
('a', 3)

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

max(items, key=second_element)
('a', 3)

برای پیدا کردن حرفی با کمترین شمارش، می‌توانیم min را به همین شکل به کار ببریم.

۱۱.۸. وارونه کردن یک دیکشنری

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

اما مشکلی هست – کلیدهای دیکشنری باید یکتا باشند، اما مقدارها لازم نیست. برای مثال، در یک شمارنده‌ی کلمه ممکن است کلمه‌های زیادی شمارش یکسانی داشته باشند.

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

d =  value_counts('parrot')
d
{'p': 1, 'a': 1, 'r': 2, 'o': 1, 't': 1}

اگر این دیکشنری را وارونه کنیم، نتیجه باید {1: ['p', 'a', 'o', 't'], 2: ['r']} باشد، که نشان می‌دهد حرف‌هایی که یک بار ظاهر می‌شوند 'p'، 'a'، 'o' و 't' هستند، و حرفی که دو بار ظاهر می‌شود 'r' است.

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

def invert_dict(d):
    new = {}
    for key, value in d.items():
        if value not in new:
            new[value] = [key]
        else:
            new[value].append(key)
    return new

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

می‌توانیم آن را این‌طور آزمایش کنیم:

invert_dict(d)
{1: ['p', 'a', 'o', 't'], 2: ['r']}

و نتیجه‌ای را که انتظار داشتیم می‌گیریم.

این اولین مثالی است که می‌بینیم مقدارهای دیکشنری فهرست‌اند. مثال‌های بیشتری هم خواهیم دید!

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

فهرست‌ها، دیکشنری‌ها و چندتایی‌ها ساختارهای داده (data structures) هستند. در این فصل شروع می‌کنیم به دیدن ساختارهای داده‌ی مرکب، مثل فهرستی از چندتایی‌ها یا دیکشنری‌هایی که چندتایی‌ها را به‌عنوان کلید و فهرست‌ها را به‌عنوان مقدار دارند. ساختارهای داده‌ی مرکب مفیدند، اما وقتی ساختار داده نوع، اندازه یا ساختار نادرستی دارد مستعد خطا هستند. برای مثال، اگر تابعی انتظار فهرستی از عددهای صحیح دارد و شما یک عدد صحیح ساده (نه در فهرست) به آن بدهید، احتمالاً کار نمی‌کند.

برای کمک به اشکال‌زدایی این نوع خطاها، ماژولی به نام structshape نوشتم که تابعی با همین نام، structshape، دارد که هر نوع ساختار داده‌ای را به‌عنوان آرگومان می‌گیرد و رشته‌ای برمی‌گرداند که ساختارش را خلاصه می‌کند. می‌توانید آن را از https://raw.githubusercontent.com/AllenDowney/ThinkPython/v3/structshape.py دانلود کنید.

download('https://raw.githubusercontent.com/AllenDowney/ThinkPython/v3/structshape.py');

می‌توانیم آن را این‌طور وارد کنیم.

from structshape import structshape

این یک مثال با فهرست ساده است.

t = [1, 2, 3]
structshape(t)
'list of 3 int'

این فهرستی از فهرست‌هاست.

t2 = [[1,2], [3,4], [5,6]]
structshape(t2)
'list of 3 list of 2 int'

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

t3 = [1, 2, 3, 4.0, '5', '6', [7], [8], 9]
structshape(t3)
'list of (3 int, float, 2 str, 2 list of int, int)'

این فهرستی از چندتایی‌هاست.

s = 'abc'
lt = list(zip(t, s))
structshape(lt)
'list of 3 tuple of (int, str)'

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

d = dict(lt) 
structshape(d)
'dict of 3 int->str'

اگر در پیگیری ساختارهای داده‌تان مشکل دارید، structshape می‌تواند کمک کند.

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

بسته‌بندی (pack): جمع کردن چند آرگومان در یک چندتایی.

بازکردن بسته (unpack): در نظر گرفتن یک چندتایی (یا دنباله‌ی دیگر) به‌عنوان چند آرگومان.

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

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

کلید مرتب‌سازی (sort key): مقدار، یا تابعی که مقداری را حساب می‌کند، که برای مرتب کردن عنصرهای یک مجموعه به کار می‌رود.

ساختار داده (data structure): مجموعه‌ای از مقدارها که طوری سازمان‌دهی شده‌اند که عملیات خاصی را کارآمد انجام دهند.

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

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

%xmode Verbose

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

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

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

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

۱۱.۱۱.۲. تمرین

در این فصل گفتم که چندتایی‌ها را می‌توان به‌عنوان کلید دیکشنری به کار برد چون درهم‌پذیرند، و درهم‌پذیرند چون تغییرناپذیرند. اما این همیشه درست نیست.

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

list0 = [1, 2, 3]
list1 = [4, 5]

t = (list0, list1)
t
([1, 2, 3], [4, 5])

یک خط کد بنویسید که مقدار 6 را به انتهای فهرست دوم در t اضافه کند. اگر t را نمایش دهید، نتیجه باید ([1, 2, 3], [4, 5, 6]) باشد.

# Solution goes here

سعی کنید دیکشنری‌ای بسازید که t را به یک رشته نگاشت می‌کند و تأیید کنید که TypeError می‌گیرید.

# Solution goes here

برای اطلاعات بیشتر درباره‌ی این موضوع، از یک دستیار مجازی بپرسید: «Are Python tuples always hashable?» (آیا چندتایی‌های پایتون همیشه درهم‌پذیرند؟)

۱۱.۱۱.۳. تمرین

در این فصل دیکشنری‌ای ساختیم که هر حرف را به اندیسش در الفبا نگاشت می‌کند.

letters = 'abcdefghijklmnopqrstuvwxyz'
numbers = range(len(letters))
letter_map = dict(zip(letters, numbers))

برای مثال، اندیس 'a' برابر 0 است.

letter_map['a']
0

برای حرکت در جهت مخالف، می‌توانیم از اندیس‌گذاری فهرست استفاده کنیم. برای مثال، حرف با اندیس 1 برابر 'b' است.

letters[1]
'b'

می‌توانیم از letter_map و letters برای رمزگذاری و رمزگشایی کلمه‌ها با رمز سزار (Caesar cipher) استفاده کنیم.

رمز سزار نوعی رمزنگاری ضعیف است که هر حرف را به تعداد ثابتی جایگاه در الفبا جابه‌جا می‌کند و در صورت لزوم به ابتدا برمی‌گردد. برای مثال، 'a' با جابه‌جایی ۲ می‌شود 'c' و 'z' با جابه‌جایی ۱ می‌شود 'a'.

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

برای آزمودن تابع‌تان، تأیید کنید که “cheer” با جابه‌جایی ۷ می‌شود “jolly” و “melon” با جابه‌جایی ۱۶ می‌شود “cubed”.

راهنمایی: از عملگر باقی‌مانده (modulus) استفاده کنید تا از 'z' به 'a' برگردید. حرف‌های کلمه را پیمایش کنید، هر کدام را جابه‌جا کنید و نتیجه را به فهرستی از حرف‌ها اضافه کنید. بعد از join برای الحاق حرف‌ها در یک رشته استفاده کنید.

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

def shift_word(word, n):
    """Shift the letters of `word` by `n` places.
    
    >>> shift_word('cheer', 7)
    'jolly'
    >>> shift_word('melon', 16)
    'cubed'
    """
    return None
# Solution goes here
shift_word('cheer', 7)
shift_word('melon', 16)

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

from doctest import run_docstring_examples

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

run_doctests(shift_word)

۱۱.۱۱.۴. تمرین

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

برای گرفتن موردها به ترتیب نزولی، می‌توانید reversed را همراه با sorted به کار ببرید یا reverse=True را به‌عنوان پارامتر کلیدواژه‌ای به sorted بدهید.

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

def most_frequent_letters(string):
    return None
# Solution goes here

و از این مثال برای آزمودن تابع‌تان.

most_frequent_letters('brontosaurus')

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

download('https://github.com/AllenDowney/ThinkPython/raw/v3/pg345.txt');

آرگومان کلیدواژه‌ای encoding='utf-8' در کتاب چاپی نیست. فقط در ویندوز لازم است.

string = open('pg345.txt', encoding='utf-8').read()
most_frequent_letters(string)

طبق کتاب «Codes and Secret Writing» نوشته‌ی Zim، دنباله‌ی حرف‌ها به ترتیب نزولی فراوانی در انگلیسی با “ETAONRISH” شروع می‌شود. این دنباله با نتیجه‌های Dracula چه تفاوتی دارد؟

۱۱.۱۱.۵. تمرین

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

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

['deltas', 'desalt', 'lasted', 'salted', 'slated', 'staled']
['retainers', 'ternaries']
['generating', 'greatening']
['resmelts', 'smelters', 'termless']

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

سلول‌های زیر words.txt را دانلود می‌کنند و کلمه‌ها را در یک فهرست می‌خوانند.

download('https://raw.githubusercontent.com/AllenDowney/ThinkPython/v3/words.txt');
word_list = open('words.txt').read().split()

این تابع sort_word است که قبلاً استفاده کرده‌ایم.

def sort_word(word):
    return ''.join(sorted(word))
# Solution goes here

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

def value_length(pair):
    key, value = pair
    return len(value)

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

anagram_items = sorted(anagram_dict.items(), key=value_length)
for key, value in anagram_items[-10:]:
    print(value)

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

longest = 7

for key, value in anagram_items:
    if len(value) > 1:
        word_len = len(value[0])
        if word_len > longest:
            longest = word_len
            print(value)

۱۱.۱۱.۶. تمرین

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

راهنمایی: از zip برای پیمایش حرف‌های متناظر کلمه‌ها استفاده کنید.

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

def word_distance(word1, word2):
    """Computes the number of places where two word differ.

    >>> word_distance("hello", "hxllo")
    1
    >>> word_distance("ample", "apply")
    2
    >>> word_distance("kitten", "mutton")
    3
    """
    return None
# Solution goes here
from doctest import run_docstring_examples

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

۱۱.۱۱.۷. تمرین

«جابه‌جایی حرف‌ها» (metathesis) یعنی جابه‌جا شدن حرف‌های یک کلمه. دو کلمه یک «جفت جابه‌جایی» (metathesis pair) می‌سازند اگر بتوانید با جابه‌جا کردن دو حرف، یکی را به دیگری تبدیل کنید، مثل converse و conserve. برنامه‌ای بنویسید که همه‌ی جفت‌های جابه‌جایی را در فهرست کلمه‌ها پیدا کند.

راهنمایی: کلمه‌های یک جفت جابه‌جایی باید آناگرام یکدیگر باشند.

منبع: این تمرین از مثالی در http://puzzlers.org الهام گرفته شده است.

# Solution goes here

۱۱.۱۱.۸. تمرین

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

این معمای دیگری از برنامه‌ی رادیویی Car Talk است (http://www.cartalk.com/content/puzzlers). (خلاصه‌ی مسئله به زبان خودم:)

طولانی‌ترین کلمه‌ی انگلیسی را پیدا کنید که اگر حرف‌هایش را یکی‌یکی حذف کنید، در هر مرحله باز هم یک کلمه‌ی معتبر انگلیسی بماند. حذف حرف از ابتدا، انتها یا وسط مجاز است، اما نمی‌توانید حرف‌ها را جابه‌جا کنید. این کار را ادامه می‌دهید تا به یک حرف تنها برسید که آن هم باید کلمه‌ی معتبر باشد. برای مثال، sprite را می‌توان به spite (حذف r از وسط)، بعد spit (حذف e از انتها)، بعد pit (حذف s)، بعد it و در پایان I کاهش داد.

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

این تمرین کمی چالش‌برانگیزتر از بیشتر تمرین‌هاست، پس این پیشنهادها را ببینید:

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

  2. به‌صورت بازگشتی، یک کلمه کاهش‌پذیر است اگر هر یک از فرزندانش کاهش‌پذیر باشد. به‌عنوان حالت پایه، می‌توانید رشته‌ی تهی را کاهش‌پذیر در نظر بگیرید.

  3. فهرست کلمه‌هایی که استفاده می‌کردیم کلمه‌ی تک‌حرفی ندارد. پس شاید لازم باشد “I” و “a” را اضافه کنید.

  4. برای بهبود کارایی برنامه‌تان، شاید بخواهید کلمه‌هایی را که کاهش‌پذیر بودنشان معلوم است یادداشت (memoize) کنید.

# Solution goes here
# Solution goes here
# Solution goes here
# Solution goes here
# Solution goes here