ساختمان داده‌ها

دیکشنری در پایتون: کلید و مقدار، get، پیمایش با items و الگوی شمارش

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

فرض کنید نمرهٔ چند دانشجو را می‌خواهید نگه دارید. با چیزهایی که تا الان بلدیم، احتمالاً دو لیست موازی می‌سازیم:

names = ['sara', 'ali', 'reza']
grades = [18, 15, 17]

# نمرهٔ ali چند است؟
i = names.index('ali')
print(grades[i])

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

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

grades = {'sara': 18, 'ali': 15, 'reza': 17}
print(grades['ali'])
print(len(grades))
15
3
لیست دیکشنری
دسترسی با اندیس عددی 0, 1, 2, … کلید دلخواه (رشته، عدد، تاپل …)
نماد [ ] {کلید: مقدار}
ترتیب دارد ترتیب درج حفظ می‌شود (پایتون ۳٫۷ به بعد)
تکرار عضو تکراری مجاز کلید تکراری ممنوع؛ مقدار تکراری مجاز
تغییرپذیر؟ بله بله

قدم اول: دیکشنری در یک نگاه

سر کلاس با همین چند خط شروع کردیم. یک دیکشنری خالی با {} ساخته می‌شود:

a = {}
print(type(a))
print(a)
<class 'dict'>
{}

حالا یک دیکشنری با سه نفر و نمره‌شان. با b[کلید] = مقدار یک نفر اضافه می‌کنیم و با b[کلید] نمرهٔ یک نفر را می‌خوانیم:

b = {"sara": 19, "ali": 20, "hassan": 7}
b["Gholam"] = 8          # افزودن
print(b)
print(b["hassan"])       # خواندن
{'sara': 19, 'ali': 20, 'hassan': 7, 'Gholam': 8}
7

با del یک کلید را (همراه مقدارش) حذف می‌کنیم. چون مقدار هر کلید مثل یک متغیر معمولی است، += هم رویش کار می‌کند:

b = {"sara": 19, "ali": 20, "hassan": 7, "Gholam": 8}
del b["ali"]             # حذف
b["hassan"] += 4         # hassan چهار نمره ارفاق گرفت
print(b)
print(b.keys())          # همهٔ کلیدها
{'sara': 19, 'hassan': 11, 'Gholam': 8}
dict_keys(['sara', 'hassan', 'Gholam'])

حلقهٔ for روی دیکشنری، کلیدها را یکی‌یکی می‌دهد و با هر کلید به مقدارش می‌رسیم:

b = {"sara": 19, "hassan": 11, "Gholam": 8}
for item in b:
    print(item, b[item])
sara 19
hassan 11
Gholam 8

ساختن دیکشنری از ورودی کاربر

مسئله: در یک حلقه اسم چند نفر را بپرسید، بعد سن هر کدام را، و در یک دیکشنری «اسم ← سن» ذخیره کنید. در پایان دیکشنری را چاپ کنید.

نکتهٔ اصلی این است که دیکشنری را قبل از حلقه خالی بسازیم و داخل حلقه هر بار یک جفت به آن اضافه کنیم:

ages = {}
for i in range(3):       # سر کلاس ۵ نفر بود؛ اینجا ۳ تا تا تایپ کمتری لازم باشد
    name = input("Enter a name: ")
    age = int(input("Enter your age: "))
    ages[name] = age
print(ages)

با ورودی‌های sara، 17، ali، 20، reza، 16:

{'sara': 17, 'ali': 20, 'reza': 16}

⚠️ input همیشه رشته برمی‌گرداند. اگر int(...) را نگذارید، دیکشنری مثل {'sara': '17'} می‌شود (با کوتیشن). بعداً که بخواهید سن‌ها را مقایسه یا جمع کنید، به دردسر می‌افتید.

💡 در اجرای داخل همین صفحه، ورودی‌ها را در پنجرهٔ بازشده پشت سر هم با | جدا کنید: sara|17|ali|20|reza|16

حالا همین کار برای نمره، و بعد چاپ نمرهٔ هر نفر در یک خط جدا:

grades = {}
for i in range(3):
    name = input("Enter a name: ")
    grade = float(input("Enter the grade: "))
    grades[name] = grade

for name in grades:
    print(name, "->", grades[name])

با ورودی‌های sara|7|ali|20|reza|12:

sara -> 7.0
ali -> 20.0
reza -> 12.0

نمره ممکن است اعشاری باشد (مثل ۱۸٫۵)، برای همین این بار به‌جای int از float استفاده کردیم.

تمرین کلاسی: نمره‌ها را اصلاح کنید

این دیکشنری را داریم (برای اینکه هر بار ورودی تایپ نکنید، نمره‌ها را مستقیم نوشته‌ایم):

grades = {"sara": 7, "Gholam": 3, "bahman": 12, "ali": 20, "taghi": 18.5}

بخش ۱. نمرهٔ همهٔ کسانی که افتاده‌اند (زیر ۱۰) را ۱۰ کنید.

grades = {"sara": 7, "Gholam": 3, "bahman": 12, "ali": 20, "taghi": 18.5}

# TODO: روی کلیدهای grades حلقه بزنید و نمره‌های زیر ۱۰ را ۱۰ کنید


# ✅ تست
assert grades == {"sara": 10, "Gholam": 10, "bahman": 12, "ali": 20, "taghi": 18.5}
print('OK ✅')
راهنمایی

for name in grades: اسم‌ها را می‌دهد. داخل حلقه، grades[name] نمرهٔ همان نفر است: هم می‌شود با آن مقایسه کرد و هم می‌شود به آن مقدار تازه داد.

جواب
grades = {"sara": 7, "Gholam": 3, "bahman": 12, "ali": 20, "taghi": 18.5}

for name in grades:
    if grades[name] < 10:
        grades[name] = 10

assert grades == {"sara": 10, "Gholam": 10, "bahman": 12, "ali": 20, "taghi": 18.5}
print('OK ✅')

بخش ۲. حالا علاوه بر آن، نمرهٔ کسانی که بالای ۱۸ گرفته‌اند را ۲۰ کنید. هر دو کار را در همان یک حلقه انجام دهید.

grades = {"sara": 7, "Gholam": 3, "bahman": 12, "ali": 20, "taghi": 18.5}

# TODO: زیر ۱۰ -> ۱۰ ، بالای ۱۸ -> ۲۰


# ✅ تست
assert grades == {"sara": 10, "Gholam": 10, "bahman": 12, "ali": 20, "taghi": 20}
print('OK ✅')
راهنمایی

یک نمره نمی‌تواند هم زیر ۱۰ باشد هم بالای ۱۸؛ پس به‌جای دو if جدا، if و elif طبیعی‌تر است. آیا ۱۸ خودش باید ۲۰ شود؟ صورت سؤال می‌گوید «بالای ۱۸»، پس > نه >=.

جواب
grades = {"sara": 7, "Gholam": 3, "bahman": 12, "ali": 20, "taghi": 18.5}

for name in grades:
    if grades[name] < 10:
        grades[name] = 10
    elif grades[name] > 18:
        grades[name] = 20

print(grades)
assert grades == {"sara": 10, "Gholam": 10, "bahman": 12, "ali": 20, "taghi": 20}
print('OK ✅')
{'sara': 10, 'Gholam': 10, 'bahman': 12, 'ali': 20, 'taghi': 20}
OK ✅

عوض کردن مقدار کلیدها وسط حلقه بی‌خطر است. اما اضافه یا حذف کردن کلید وسط حلقه خطا می‌دهد (پایین‌تر می‌بینیم).

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


تا اینجا اصل ماجرا را دیدیم: ساختن، افزودن، خواندن، حذف و حلقه. بقیهٔ این صفحه هر کدام از این‌ها را دقیق‌تر بررسی می‌کند و ابزارهای تازه‌ای مثل get، items و الگوی شمارش را معرفی می‌کند.

ساختن دیکشنری: راه‌های دیگر

empty = {}               # دیکشنری خالی (نه مجموعه!)
also_empty = dict()

phone = {'sara': '0912', 'ali': '0935'}
person = dict(name='Mina', age=21)      # کلیدها رشته می‌شوند
pairs = dict([('x', 1), ('y', 2)])      # از لیستِ جفت‌ها

print(phone)
print(person)
print(pairs)
{'sara': '0912', 'ali': '0935'}
{'name': 'Mina', 'age': 21}
{'x': 1, 'y': 2}

اگر یک کلید را دوبار بنویسید، دومی جای اولی را می‌گیرد، چون هر کلید فقط یک مقدار دارد:

d = {'a': 1, 'b': 2, 'a': 99}
print(d)
{'a': 99, 'b': 2}

خواندن: [] در برابر get

با d[key] مقدار را می‌گیریم. اما اگر کلید وجود نداشته باشد، برنامه با KeyError می‌ایستد:

grades = {'sara': 18, 'ali': 15}
print(grades['mina'])
KeyError: 'mina'

دو راه برای جلوگیری داریم:

grades = {'sara': 18, 'ali': 15}

# ۱) اول بپرس، بعد بخوان
if 'mina' in grades:
    print(grades['mina'])
else:
    print('نمره‌ای ثبت نشده')

# ۲) get: اگر نبود، مقدار پیش‌فرض برگردان (پیش‌فرضِ پیش‌فرض None است)
print(grades.get('mina'))
print(grades.get('mina', 0))
print(grades.get('sara', 0))
نمره‌ای ثبت نشده
None
0
18

⚠️ دام رایج: عملگر in روی دیکشنری فقط کلیدها را می‌گردد، نه مقدارها. 18 in grades برابر False است، حتی اگر نمرهٔ کسی ۱۸ باشد. برای گشتن مقدارها: 18 in grades.values().

افزودن، تغییر و حذف

دیکشنری تغییرپذیر است. برای افزودن کلید تازه و برای تغییر مقدارِ کلید موجود، از همان نحو استفاده می‌کنیم:

grades = {'sara': 18}
grades['ali'] = 15        # کلید نبود -> اضافه می‌شود
grades['sara'] = 19       # کلید بود -> مقدارش عوض می‌شود
print(grades)

del grades['ali']         # حذف (اگر نباشد KeyError)
print(grades)

g = grades.pop('sara')    # حذف و برگرداندن مقدار
print(g, grades)

print(grades.pop('nobody', 'نبود'))   # pop هم پیش‌فرض می‌گیرد
{'sara': 19, 'ali': 15}
{'sara': 19}
19 {}
نبود

برای ادغام یک دیکشنری در دیگری، update (یا در پایتون ۳٫۹ به بعد عملگر |) به کار می‌آید:

base = {'color': 'blue', 'size': 'M'}
base.update({'size': 'L', 'price': 120})
print(base)

print({'a': 1} | {'b': 2})
{'color': 'blue', 'size': 'L', 'price': 120}
{'a': 1, 'b': 2}

پیمایش: keys، values و items

حلقهٔ for روی خود دیکشنری، کلیدها را می‌دهد:

grades = {'sara': 18, 'ali': 15, 'reza': 17}

for name in grades:
    print(name, grades[name])
sara 18
ali 15
reza 17

ولی اغلب کلید و مقدار را با هم می‌خواهیم. items() جفت‌های (کلید, مقدار) را می‌دهد و با unpacking تاپل مستقیم در دو متغیر می‌ریزیم:

grades = {'sara': 18, 'ali': 15, 'reza': 17}

for name, g in grades.items():
    print(f'{name}: {g}')

print(list(grades.keys()))
print(list(grades.values()))
print(sum(grades.values()) / len(grades))
sara: 18
ali: 15
reza: 17
['sara', 'ali', 'reza']
[18, 15, 17]
16.666666666666668

پیدا کردن «بهترین» کلید هم الگوی رایجی است. max با key=grades.get کلیدی را برمی‌گرداند که مقدارش بیشتر است:

grades = {'sara': 18, 'ali': 15, 'reza': 17}
best = max(grades, key=grades.get)
print(best, grades[best])

# مرتب‌سازی بر اساس مقدار (از زیاد به کم)
for name, g in sorted(grades.items(), key=lambda p: p[1], reverse=True):
    print(name, g)
sara 18
sara 18
reza 17
ali 15

⚠️ وسط پیمایش یک دیکشنری، کلید اضافه یا حذف نکنید؛ پایتون خطای RuntimeError: dictionary changed size during iteration می‌دهد. مثل دام لیست‌ها، روی یک کپی پیمایش کنید: for k in list(d):

الگوی شمارش (مهم‌ترین کاربرد)

شمردن تعداد تکرار هر چیز (هر حرف، هر کلمه، هر رأی) کاری است که دیکشنری برایش ساخته شده. سه نسخهٔ معادل:

text = 'banana'

# نسخهٔ ۱: if/else
count = {}
for ch in text:
    if ch in count:
        count[ch] += 1
    else:
        count[ch] = 1
print(count)

# نسخهٔ ۲: get (کوتاه‌تر و رایج‌تر)
count = {}
for ch in text:
    count[ch] = count.get(ch, 0) + 1
print(count)

# نسخهٔ ۳: Counter از کتابخانهٔ استاندارد
from collections import Counter
print(Counter(text))
print(Counter(text).most_common(1))
{'b': 1, 'a': 3, 'n': 2}
{'b': 1, 'a': 3, 'n': 2}
Counter({'a': 3, 'n': 2, 'b': 1})
[('a', 3)]

اول نسخهٔ ۲ را خوب یاد بگیرید؛ Counter را وقتی به کار ببرید که بدانید پشتش چه می‌گذرد.

مقدارها می‌توانند هر چیزی باشند

مقدارِ یک کلید می‌تواند لیست، تاپل یا حتی دیکشنری دیگر باشد. مثلاً گروه‌بندی کلمه‌ها بر اساس حرف اول:

words = ['apple', 'avocado', 'banana', 'blueberry', 'cherry']
groups = {}
for w in words:
    first = w[0]
    if first not in groups:
        groups[first] = []          # اولین بار: لیست خالی بساز
    groups[first].append(w)
print(groups)

# همین کار در یک خط با setdefault:
groups = {}
for w in words:
    groups.setdefault(w[0], []).append(w)
print(groups['b'])
{'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}
['banana', 'blueberry']

سر کلاس دیدیم که مقدار می‌تواند خودش یک دیکشنری باشد: نمرهٔ هر درسِ هر دانشجو.

grades = {"ali": {"physics": 20, "chemistry": 17}, "sara": {"art": 20}}
print(grades["ali"].keys())
for course in grades["ali"]:
    print(course)
grades["ali"]["physics"] -= 2      # دو کروشه: اول دانشجو، بعد درس
print(grades)
dict_keys(['physics', 'chemistry'])
physics
chemistry
{'ali': {'physics': 18, 'chemistry': 17}, 'sara': {'art': 20}}

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

students = {
    'sara': {'age': 20, 'grades': [18, 19]},
    'ali':  {'age': 22, 'grades': [14, 16, 15]},
}
print(students['ali']['grades'][-1])

for name, info in students.items():
    avg = sum(info['grades']) / len(info['grades'])
    print(f'{name} ({info["age"]}): {avg:.1f}')
15
sara (20): 18.5
ali (22): 15.0

چه چیزی می‌تواند کلید باشد؟

کلید باید تغییرناپذیر (دقیق‌تر: hashable) باشد: عدد، رشته، True/False، تاپلی از این‌ها. لیست و دیکشنری نمی‌توانند کلید باشند. پایتون جای هر کلید را از روی «اثر انگشتِ» (hash) آن حساب می‌کند. اگر کلید بعداً عوض شود، دیگر پیدایش نمی‌کند.

dist = {('tehran', 'shiraz'): 935, ('tehran', 'tabriz'): 630}   # تاپل: مجاز
print(dist[('tehran', 'tabriz')])

try:
    bad = {['tehran', 'shiraz']: 935}                          # لیست: ممنوع
except TypeError as e:
    print('خطا:', e)
630
خطا: cannot use 'list' as a dict key (unhashable type: 'list')

متن دقیق پیام خطا ممکن است در نسخه‌های مختلف پایتون کمی فرق کند؛ مهم کلمهٔ unhashable است.

دام alias دوباره

دیکشنری هم مثل لیست تغییرپذیر است، پس همان دام alias اینجا هم هست. b = a کپی نمی‌سازد، فقط اسم دوم می‌گذارد:

a = {'x': 1}
b = a            # alias
c = a.copy()     # کپی (سطحی)
a['x'] = 100
print(b, c)
{'x': 100} {'x': 1}

💡 copy() کپی سطحی است: اگر مقدارها لیست باشند، لیست‌های داخلی هنوز مشترک‌اند. برای کپی کامل: import copy; copy.deepcopy(d).

dictionary comprehension (نگاه کوتاه)

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

squares = {n: n * n for n in range(1, 6)}
print(squares)

prices = {'pen': 20, 'book': 150, 'bag': 400}
cheap = {k: v for k, v in prices.items() if v < 200}
print(cheap)

flipped = {v: k for k, v in prices.items()}   # جای کلید و مقدار عوض
print(flipped[150])
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
{'pen': 20, 'book': 150}
book

خلاصه

کار کد
ساخت d = {'a': 1} یا dict()
خواندن (با خطا اگر نبود) d['a']
خواندن امن d.get('a', 0)
وجود کلید 'a' in d
افزودن / تغییر d['a'] = 5
حذف del d['a'] · d.pop('a', None)
پیمایش for k, v in d.items():
شمارش d[x] = d.get(x, 0) + 1
گروه‌بندی d.setdefault(k, []).append(x)
کپی d.copy()

تمرین‌ها (خودآموز، پله‌پله)

تمرین‌ها به ترتیب سخت‌تر می‌شوند. در هر تمرین، اسکلت را همین‌جا با دکمهٔ «ویرایش» پر کنید و اجرا کنید. اگر درست نوشته باشید OK ✅ چاپ می‌شود؛ وگرنه AssertionError می‌گیرید و می‌فهمید کدام تست رد شده. فقط وقتی سراغ «جواب» بروید که واقعاً گیر کرده‌اید. اول «راهنمایی» را باز کنید.

سطح ۱: گرم‌کردن

تمرین ۱. دفترچهٔ تلفن. با این دیکشنری کارهای زیر را به ترتیب انجام دهید:

phone = {'sara': '0912-111', 'ali': '0935-222'}

# الف) شمارهٔ reza را '0901-333' اضافه کنید
# ب) شمارهٔ ali را به '0935-999' تغییر دهید
# ج) sara را حذف کنید
# د) شمارهٔ mina را با get بخوانید طوری که اگر نبود 'ندارد' برگردد و در result بریزید
result = None


# ✅ تست
assert phone == {'ali': '0935-999', 'reza': '0901-333'}
assert result == 'ندارد'
print('OK ✅')
جواب
phone = {'sara': '0912-111', 'ali': '0935-222'}
phone['reza'] = '0901-333'
phone['ali'] = '0935-999'
del phone['sara']
result = phone.get('mina', 'ندارد')

assert phone == {'ali': '0935-999', 'reza': '0901-333'}
assert result == 'ندارد'
print('OK ✅')

دقت کنید که ترتیب کلیدها در مقایسهٔ == مهم نیست؛ دو دیکشنری وقتی برابرند که جفت‌های یکسان داشته باشند.

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

d = {'a': 1, 'b': 2}
print('a' in d)
print(2 in d)
print(2 in d.values())
print(d.get('c', 'x'))
d['c'] = d.get('c', 0) + 10
print(d)
print(len(d))
جواب
True
False
True
x
{'a': 1, 'b': 2, 'c': 10}
3

2 in d برابر False است چون in فقط کلیدها را می‌گردد.

تمرین ۳. میانگین نمره‌ها. تابع average(grades) یک دیکشنریِ «اسم ← نمره» بگیرد و میانگین نمره‌ها را برگرداند. اگر دیکشنری خالی بود، 0 برگرداند.

def average(grades):
    # TODO
    pass


# ✅ تست
assert average({'sara': 18, 'ali': 15, 'reza': 18}) == 17
assert average({'x': 10}) == 10
assert average({}) == 0
print('OK ✅')
راهنمایی

grades.values() همهٔ نمره‌ها را می‌دهد؛ sum و len روی آن کار می‌کنند. حالت خالی را اول بررسی کنید تا تقسیم بر صفر پیش نیاید.

جواب
def average(grades):
    if not grades:          # دیکشنری خالی False حساب می‌شود
        return 0
    return sum(grades.values()) / len(grades)

assert average({'sara': 18, 'ali': 15, 'reza': 18}) == 17
assert average({'x': 10}) == 10
assert average({}) == 0
print('OK ✅')

سطح ۲: الگوهای اصلی

تمرین ۴. شمارش حروف. تابع letter_count(s) تعداد تکرار هر حرف رشتهٔ s را در یک دیکشنری برگرداند. فاصله‌ها را نشمارید و حروف بزرگ و کوچک را یکی حساب کنید. از Counter استفاده نکنید.

def letter_count(s):
    # TODO
    pass


# ✅ تست
assert letter_count('Hello') == {'h': 1, 'e': 1, 'l': 2, 'o': 1}
assert letter_count('a A a') == {'a': 3}
assert letter_count('') == {}
print('OK ✅')
راهنمایی

همان «الگوی شمارش» بالای صفحه است، با دو کار اضافه: قبل از حلقه حروف را کوچک کنید، و داخل حلقه اگر به فاصله رسیدید، از آن رد شوید. کدام دستور یک دور حلقه را رد می‌کند؟

جواب
def letter_count(s):
    count = {}
    for ch in s.lower():
        if ch == ' ':
            continue
        count[ch] = count.get(ch, 0) + 1
    return count

assert letter_count('Hello') == {'h': 1, 'e': 1, 'l': 2, 'o': 1}
assert letter_count('a A a') == {'a': 3}
assert letter_count('') == {}
print('OK ✅')

تمرین ۵. پرتکرارترین کلمه. تابع most_common_word(text) کلمه‌ای را برگرداند که بیشترین تکرار را دارد (کلمه‌ها با فاصله جدا شده‌اند؛ بزرگی و کوچکی حروف مهم نیست).

def most_common_word(text):
    # TODO
    pass


# ✅ تست
assert most_common_word('the cat and the dog and THE bird') == 'the'
assert most_common_word('python') == 'python'
print('OK ✅')
راهنمایی

مسئله را دو تکه کنید: ① دیکشنری «کلمه ← تعداد» بسازید (متن را به کلمه بشکنید و الگوی شمارش را به کار ببرید). ② کلیدی را پیدا کنید که بیشترین مقدار را دارد. برای تکهٔ دوم، مثال max با key= را در بخش «پیمایش» همین صفحه دوباره بخوانید، یا با یک حلقه، «بهترینِ تا اینجا» را نگه دارید.

جواب
def most_common_word(text):
    count = {}
    for w in text.lower().split():
        count[w] = count.get(w, 0) + 1
    return max(count, key=count.get)

assert most_common_word('the cat and the dog and THE bird') == 'the'
assert most_common_word('python') == 'python'
print('OK ✅')

نسخهٔ بدون max(key=…):

def most_common_word(text):
    count = {}
    for w in text.lower().split():
        count[w] = count.get(w, 0) + 1
    best, best_count = None, 0
    for w, c in count.items():
        if c > best_count:
            best, best_count = w, c
    return best

print(most_common_word('a b b c c c'))

تمرین ۶. وارونه کردن دیکشنری. تابع invert(d) جای کلید و مقدار را عوض کند. اما چون چند کلید ممکن است مقدار یکسان داشته باشند، هر مقدار باید به لیستی از کلیدها نگاشت شود.

def invert(d):
    # TODO
    pass


# ✅ تست
grades = {'sara': 18, 'ali': 15, 'reza': 18, 'mina': 20}
assert invert(grades) == {18: ['sara', 'reza'], 15: ['ali'], 20: ['mina']}
assert invert({}) == {}
print('OK ✅')
راهنمایی

این همان «گروه‌بندی» بخش «مقدارها می‌توانند هر چیزی باشند» است: این بار مقدارِ قدیم کلید جدید می‌شود و کلیدِ قدیم به لیستش اضافه می‌شود. اول ببینید چرا جابه‌جایی ساده کافی نیست: {v: k for k, v in grades.items()} را اجرا کنید. sara کجا رفت؟

جواب
def invert(d):
    result = {}
    for k, v in d.items():
        result.setdefault(v, []).append(k)
    return result

grades = {'sara': 18, 'ali': 15, 'reza': 18, 'mina': 20}
assert invert(grades) == {18: ['sara', 'reza'], 15: ['ali'], 20: ['mina']}
assert invert({}) == {}
print('OK ✅')

با comprehension ساده، reza روی sara بازنویسی می‌شود و sara گم می‌شود.

تمرین ۷. سبد خرید. دیکشنری prices قیمت هر کالا و دیکشنری cart تعداد هر کالا در سبد است. تابع total(prices, cart) جمع کل را برگرداند. اگر کالایی در سبد بود که قیمتش را نداریم، آن را نادیده بگیرید.

def total(prices, cart):
    # TODO
    pass


# ✅ تست
prices = {'pen': 20, 'book': 150, 'bag': 400}
assert total(prices, {'pen': 3, 'book': 1}) == 210
assert total(prices, {'bag': 1, 'laptop': 2}) == 400
assert total(prices, {}) == 0
print('OK ✅')
جواب
def total(prices, cart):
    s = 0
    for item, qty in cart.items():
        s += prices.get(item, 0) * qty
    return s

prices = {'pen': 20, 'book': 150, 'bag': 400}
assert total(prices, {'pen': 3, 'book': 1}) == 210
assert total(prices, {'bag': 1, 'laptop': 2}) == 400
assert total(prices, {}) == 0
print('OK ✅')

get(item, 0) باعث می‌شود کالای بی‌قیمت صفر حساب شود، بدون if و بدون KeyError.

سطح ۳: ترکیبی

تمرین ۸. گروه‌بندی جناس‌ها (anagram). دو کلمه جناس‌اند اگر از حروف یکسان ساخته شده باشند (listen و silent). تابع group_anagrams(words) لیستی از گروه‌ها برگرداند؛ هر گروه لیستی از کلمه‌های جناس هم است، به ترتیبی که در ورودی آمده‌اند.

def group_anagrams(words):
    # TODO
    pass


# ✅ تست
words = ['listen', 'silent', 'cat', 'enlist', 'act', 'dog']
assert group_anagrams(words) == [['listen', 'silent', 'enlist'], ['cat', 'act'], ['dog']]
assert group_anagrams([]) == []
print('OK ✅')
راهنمایی ۱

به یک «امضا» برای هر کلمه نیاز دارید که برای همهٔ جناس‌ها یکسان باشد. اگر حروف کلمه را مرتب کنید چه می‌شود؟ sorted('listen') را امتحان کنید.

راهنمایی ۲

sorted یک لیست برمی‌گرداند و لیست نمی‌تواند کلید باشد. یا با ''.join(...) آن را رشته کنید یا با tuple(...) تاپل. بعد: دیکشنری «امضا ← لیست کلمه‌ها» و در آخر list(d.values()).

جواب
def group_anagrams(words):
    groups = {}
    for w in words:
        key = ''.join(sorted(w))
        groups.setdefault(key, []).append(w)
    return list(groups.values())

words = ['listen', 'silent', 'cat', 'enlist', 'act', 'dog']
assert group_anagrams(words) == [['listen', 'silent', 'enlist'], ['cat', 'act'], ['dog']]
assert group_anagrams([]) == []
print('OK ✅')

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

تمرین ۹. کارنامه. لیستی از تاپل‌های (اسم, درس, نمره) داریم. تابع report(records) یک دیکشنری تودرتو برگرداند: «اسم ← {درس ← نمره}». سپس تابع best_student(records) اسم کسی را برگرداند که میانگین نمره‌هایش بیشتر است.

def report(records):
    # TODO
    pass

def best_student(records):
    # TODO (می‌توانید از report استفاده کنید)
    pass


# ✅ تست
records = [
    ('sara', 'math', 18), ('ali', 'math', 15),
    ('sara', 'physics', 16), ('ali', 'physics', 20),
    ('reza', 'math', 19),
]
assert report(records) == {
    'sara': {'math': 18, 'physics': 16},
    'ali': {'math': 15, 'physics': 20},
    'reza': {'math': 19},
}
assert best_student(records) == 'reza'
print('OK ✅')
راهنمایی

در report، هر رکورد یک تاپل سه‌تایی است؛ با unpacking در سه متغیر بازش کنید. اگر برای این اسم هنوز دیکشنری درونی نساخته‌اید، اول یک دیکشنری خالی برایش بسازید، بعد درس را در آن ثبت کنید. در best_student، نتیجهٔ report را بگیرید و برای هر نفر میانگین مقدارهای دیکشنری درونی‌اش را حساب کنید. بعد همان کار تمرین ۵: کلیدِ بیشینه.

جواب
def report(records):
    result = {}
    for name, course, grade in records:
        result.setdefault(name, {})[course] = grade
    return result

def best_student(records):
    rep = report(records)
    avg = {name: sum(c.values()) / len(c) for name, c in rep.items()}
    return max(avg, key=avg.get)

records = [
    ('sara', 'math', 18), ('ali', 'math', 15),
    ('sara', 'physics', 16), ('ali', 'physics', 20),
    ('reza', 'math', 19),
]
assert report(records) == {
    'sara': {'math': 18, 'physics': 16},
    'ali': {'math': 15, 'physics': 20},
    'reza': {'math': 19},
}
assert best_student(records) == 'reza'
print('OK ✅')

میانگین‌ها: sara ۱۷، ali ۱۷٫۵، reza ۱۹.

🏆 چالش پایانی: کش (memo) برای فیبوناچی

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

calls = 0
def fib(n):
    global calls
    calls += 1
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(20), 'با', calls, 'فراخوانی')

حالا fib_memo(n, memo) را بنویسید که هر جوابی را که یک بار حساب کرد، در دیکشنری memo نگه دارد و دفعهٔ بعد مستقیم از آنجا بخواند.

def fib_memo(n, memo=None):
    if memo is None:
        memo = {}
    # TODO 1: اگر n در memo هست، همان را برگردانید
    # TODO 2: حالت پایه
    # TODO 3: حساب کنید، در memo[n] بریزید، برگردانید
    pass


# ✅ تست
assert fib_memo(10) == 55
assert fib_memo(100) == 354224848179261915075
print('OK ✅')
جواب
def fib_memo(n, memo=None):
    if memo is None:
        memo = {}
    if n in memo:
        return memo[n]
    if n < 2:
        return n
    memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
    return memo[n]

assert fib_memo(10) == 55
assert fib_memo(100) == 354224848179261915075
print('OK ✅')

نسخهٔ ساده برای fib(100) تا پایان عمر جهان طول می‌کشد؛ با memo هر n فقط یک بار حساب می‌شود. این ایده (ذخیرهٔ جواب زیرمسئله‌ها) پایهٔ «برنامه‌نویسی پویا» است. چرا memo={} را مستقیم پیش‌فرض نگذاشتیم؟ چون مقدار پیش‌فرض تغییرپذیر یک بار ساخته می‌شود و بین همهٔ فراخوانی‌ها مشترک می‌ماند. اینجا بی‌ضرر بود، ولی عادت خوبی نیست.

برای مطالعهٔ بیشتر و تمرین بیشتر

درس بعد: مجموعه (set)، که مثل دیکشنریِ «فقط کلید» است.