القواميس — أزواج المفاتيح والقيم
تخزين القيم وجلبها بالاسم، تجنب KeyError باستخدام .get()، إضافة المفاتيح وتعديلها وحذفها، التكرار باستخدام .items()، ولماذا لا يمكن للقائمة أن تكون مفتاحاً.
كيف تريد أن تتعلم؟
المشكلة التي نقوم بحلها
علينا حفظ درجات ثلاثة أشخاص — ومعها من صاحب كل درجة.
بالقوائم (lists) توجد طريقة واحدة: قائمتان، جنباً إلى جنب.
names = ["rafi", "ahmed", "bilal"]
marks = [72, 45, 90]
print(names[1], marks[1])ahmed 45هذا يعمل. لكن هذا الترتيب كله قائم على انضباطك أنت، ولا تقدم بايثون هنا أي مساعدة.
ماذا لو أضفت شخصاً إلى names ونسيت marks؟ يصبح طول القائمتين مختلفاً، ثم يتحدث names[2] وmarks[2] عن شخصين مختلفين. لن تقول بايثون شيئاً. وماذا لو رتّبت names؟ تنكسر العلاقة تماماً، بصمت.
أصل المشكلة هنا: العلاقة بين الشيئين غير مكتوبة في أي مكان. العلاقة مجرد “الفهرس نفسه” — مصادفة يمكن أن تنكسر في أي لحظة.
القاموس (dictionary) يكتب هذه العلاقة في الكود. اسم بدل الفهرس، والزوج مربوط في مكان واحد.
في نهاية هذا الفصل ستكون قادراً على
- إنشاء قاموس، واستخراج القيمة (value) بالمفتاح (key)
- استخدام
.get()لتجنبKeyError، ومعرفة متى يكون كل منهما صحيحاً - إضافة مفتاح، وتغييره، وحذفه
- المرور على القاموس باستخدام
.keys()و.values()و.items() - شرح لماذا لا يمكن للقائمة أن تكون مفتاحاً، بينما يمكن للصف (tuple) ذلك
المتطلبات السابقة: الصفوف (tuples) وفك الحزم (unpacking).
القاموس — أزواج من مفتاح وقيمة
أقواس معقوفة، وبداخلها أزواج key: value:
marks = {"rafi": 72, "ahmed": 45, "bilal": 90}
print(marks)
print(marks["ahmed"])
print(len(marks)){'rafi': 72, 'ahmed': 45, 'bilal': 90}
45
3marks["ahmed"] — ليس فهرساً بل اسماً. والاسم مكتوب في الكود نفسه، لذلك حتى بعد ستة أشهر تفهم من القراءة ما المطلوب.
تعدّ len() الأزواج، وعددها ثلاثة.
كلمة عن الترتيب. منذ بايثون 3.7 يتذكر القاموس الترتيب الذي أُدخلت به العناصر، ويطبعها بهذا الترتيب. لكن القاموس يُستخدم للبحث بالاسم، لا بالموضع — اكتبmarks[0]وستبحث بايثون عن مفتاح هو الرقم صفر، ولن تجده، فتتوقف بـKeyError: 0.
مفتاح غير موجود — KeyError و.get()
marks = {"rafi": 72}
print(marks["dia"])KeyError: 'dia'يعني KeyError: “لا يوجد مفتاح بهذا الاسم”. في القائمة كان الخطأ نفسه سيعطي IndexError — اسم مختلف، والنوع نفسه من الخطأ.
إذا أردت جواباً آمناً بدل التوقف، استخدم .get():
marks = {"rafi": 72}
print(marks.get("dia"))
print(marks.get("dia", 0))
print(marks.get("rafi", 0))None
0
72الوسيط الثاني معناه “إذا لم تجده فأعطني هذا”. وإذا لم تعطه، تعود None.
أيهما ومتى؟ إذا كان يُفترض أن يكون المفتاح موجوداً، فاكتب marks["rafi"] — غيابه خطأ برمجي، وKeyError تكشفه لك فوراً. وإذا كان غياب المفتاح أمراً طبيعياً، فاستخدم .get(). إذا استخدمت .get() في كل مكان، فستمر البيانات الخاطئة بصمت أيضاً، وهي بالضبط ما كنت تريد العثور عليه.
الإضافة والتغيير والحذف
marks = {"rafi": 72}
marks["dia"] = 88
marks["rafi"] = 75
print(marks)
del marks["rafi"]
print(marks){'rafi': 75, 'dia': 88}
{'dia': 88}السطر نفسه يقوم بعملين: إذا لم يكن المفتاح موجوداً يُضاف، وإذا كان موجوداً تتغير قيمته. لا توجد دالة عضو منفصلة للإضافة.
ومن هنا تأتي قاعدة مهمة — المفتاح لا يوجد إلا مرة واحدة:
marks = {"rafi": 72, "rafi": 90}
print(marks){'rafi': 90}لا يوجد خطأ. الثاني محا الأول بصمت. يحدث هذا كثيراً عند قراءة البيانات من ملف، وهو سبب شائع لضياع السجلات.
in تبحث في المفاتيح، لا في القيم
marks = {"rafi": 72, "ahmed": 45}
print("rafi" in marks)
print(72 in marks)True
False72 موجودة كقيمة فعلاً، لكن in تبحث في المفاتيح فقط. للبحث في القيم يجب أن تقول ذلك صراحة: 72 in marks.values().
المرور على القاموس — بثلاث طرق
marks = {"rafi": 72, "ahmed": 45}
for name in marks:
print(name)
print("---")
for name, mark in marks.items():
print(name, mark)
print("---")
print(list(marks.keys()))
print(list(marks.values()))rafi
ahmed
---
rafi 72
ahmed 45
---
['rafi', 'ahmed']
[72, 45]الأولى تستحق الانتباه: for name in marks تعطيك المفاتيح، لا القيم. هذا يفاجئ الجميع في المرة الأولى.
إذا احتجت إلى الاثنين فاستخدم .items()، التي تعطي صفاً في كل دورة — وname, mark تفك حزمته. فك الحزم من الفصل السابق أكثر ما يفيد هنا.
للحساب على القيم استخدم .values():
marks = {"rafi": 72, "ahmed": 45, "bilal": 90}
print(sum(marks.values()))
print(max(marks.values()))
print(round(sum(marks.values()) / len(marks), 2))207
90
69.0ما الذي يمكن أن يكون مفتاحاً
لكي يكون الشيء مفتاحاً يجب أن يكون غير قابل للتعديل. السلاسل النصية والأرقام والصفوف — تصلح. القائمة لا تصلح:
d = {}
d[["a", "b"]] = 1TypeError: unhashable type: 'list'كلمة “Unhashable” تحتاج إلى شرح. يجد القاموس الأشياء بسرعة لأنه يحسب من كل مفتاح رقماً (hash) ويحفظه، ثم يصل بذلك الرقم مباشرة إلى المكان الصحيح. لو تغيّر المفتاح لاحقاً لأصبح ذلك الحساب خاطئاً، ولضاع العنصر. لذلك لا تسمح بايثون لأي شيء قابل للتعديل بأن يكون مفتاحاً.
الصف لا يمكن تعديله، لذلك هو مفتاح ممتاز:
grid = {}
grid[(0, 0)] = "start"
grid[(1, 2)] = "tree"
print(grid)
print(grid[(1, 2)]){(0, 0): 'start', (1, 2): 'tree'}
treeفي الفصل السابق وُصف عدم قابلية الصف للتعديل بأنه ميزة. هذا أول دليل عملي على ذلك.
النمط الأكثر فائدة — العدّ
words = ["pen", "bag", "pen", "ink", "pen"]
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts){'pen': 3, 'bag': 1, 'ink': 1}اقرأ السطر من الداخل: تقول counts.get(word, 0) “أعطني العدد الحالي لهذه الكلمة، واعتبره صفراً إن لم يكن موجوداً”، ثم يُضاف إليه واحد ويُحفظ من جديد.
القيمة الافتراضية في .get() هي التي تقوم بكل العمل هنا — لولاها لظهر KeyError مع كل كلمة جديدة، ولاحتجت إلى كتابة if word in counts: ومعالجة فرعين. ستكتب هذا النمط ذا السطر الواحد مرات كثيرة.
مثال كامل
marks.py:
# A dictionary keeps a name attached to its value
marks = {"rafi": 72, "ahmed": 45, "bilal": 90, "dia": 88}
print("Everyone:")
for name, mark in marks.items():
status = "pass" if mark >= 40 else "fail"
print(f" {name:<8} {mark:>3} {status}")
print()
print("Count :", len(marks))
print("Total :", sum(marks.values()))
print("Average :", round(sum(marks.values()) / len(marks), 2))
print("Highest :", max(marks.values()))
best = max(marks, key=marks.get)
print("Best :", best, marks[best])
print("Has dia :", "dia" in marks)
print("Missing :", marks.get("nadia", "not recorded"))
marks["dia"] = 91
marks["nadia"] = 60
del marks["ahmed"]
print()
print("After edits:", marks)Everyone:
rafi 72 pass
ahmed 45 pass
bilal 90 pass
dia 88 pass
Count : 4
Total : 295
Average : 73.75
Highest : 90
Best : bilal 90
Has dia : True
Missing : not recorded
After edits: {'rafi': 72, 'bilal': 90, 'dia': 91, 'nadia': 60}ثلاثة أمور تستحق الانتباه.
status = "pass" if mark >= 40 else "fail" — طريقة لكتابة شرط في سطر واحد، ويُقرأ هكذا: “إذا كان mark >= 40 فـ "pass"، وإلا "fail"”. جملة if/else ذات الأسطر الأربعة من الفصل 9 تختار هنا قيمة واحدة فقط، لذلك يكفي سطر واحد. للقرارات الكبيرة اكتب if كاملة — هذا الشكل لاختيار قيمة من اثنتين فقط.
max(marks, key=marks.get) — key= من الفصل 13 في عمل جديد. (هنا key= هو اسم وسيط في max، وليس مفتاحاً في القاموس.) كانت max(marks) ستعطي أكبر مفتاح، أي آخر اسم أبجدياً. تقول key=marks.get: قارن كل مفتاح بـقيمته — لذلك يأتي اسم صاحب أعلى درجة. لاحظ أن الحصول على أعلى درجة سهل (max(marks.values()))، لكن معرفة صاحبها تحتاج إلى هذه الطريقة.
الترتيب في السطر الأخير. كان dia موجوداً من قبل، فتغيّرت قيمته ولم يتغير مكانه. nadia جديد، فأُضيف في النهاية. وahmed حُذف. يتذكر القاموس ترتيب الإدخال — وتغيير قيمة لا يحرّك ذلك الترتيب.
حالات الخطأ الشائعة وحلولها
KeyError: 'dia' لا يوجد مفتاح بهذا الاسم. راجع الإملاء وحالة الأحرف — "Rafi" و"rafi" مفتاحان مختلفان. المفاتيح المقروءة من ملف كثيراً ما تحمل مسافة في نهايتها؛ إذا نظرت بـ print(list(d.keys())) تظهر داخل علامات الاقتباس. إذا كان غياب المفتاح طبيعياً فاستخدم .get().
TypeError: unhashable type: 'list' محاولة جعل قائمة مفتاحاً. استخدم صفاً — tuple(my_list) تنشئه لك.
for x in my_dict لا تعطيني القيم ولا يُفترض أن تعطيها — إنها تعطي المفاتيح. للقيم استخدم .values()، وللاثنين .items().
كان المفتاحان متطابقين فضاع سجل المفتاح لا يوجد إلا مرة واحدة، واللاحق يستبدل السابق بصمت. لحفظ عدة سجلات تحت اسم واحد، اجعل القيمة قائمة — وهذا موضوع الفصل 17.
KeyError: 0 كُتب my_dict[0]، لطلب العنصر الأول كما في القائمة. لا توجد مواضع في القاموس؛ يُبحث عن 0 كمفتاح، وإذا لم يوجد مثل هذا المفتاح يظهر KeyError: 0. إذا احتجت فعلاً إلى الزوج الأول، فاكتب list(my_dict.items())[0].
.sort() لا تعمل على القاموس صحيح — لا توجد .sort() في القاموس. تعطي sorted(d.items()) قائمة مرتبة، وهي عادة ما تحتاجه.
الخطوة ٤ من ٦ — التوقع
تحقّق من فهمك
حلقة تمر مباشرة على قاموس. ماذا يُطبع؟
marks = {"rafi": 72, "ahmed": 45}
for name in marks:
print(name)- Arafi 72 ahmed 45
- Brafi ahmed
- C72 45
- D('rafi', 72) ('ahmed', 45)
طُلب مفتاح غير موجود. ماذا يحدث، وما البديل الآمن؟
marks = {"rafi": 72}
print(marks["dia"])- Aيطبع `None`
- Bيطبع سطراً فارغاً
- Cخطأ `KeyError` — البديل الآمن هو `marks.get("dia")`، الذي يعطي `None`
- Dخطأ `IndexError`
نمط العدّ. ماذا يُطبع؟
words = ["pen", "bag", "pen"]
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts)- Aخطأ `KeyError`
- B{'pen': 2, 'bag': 1}
- C{'pen': 1, 'bag': 1}
- D{'pen': 3, 'bag': 1}
الإجابة تتطلب حساباً
سجّل الدخول للتحقق من إجاباتك
الأسئلة موجودة أعلاه، والتفكير في إجاباتها ذهنياً هو الجزء المهم. سجّل الدخول لرؤية الإجابات والتفسيرات والتلميحات ذات المستويات الثلاثة.
دورك الآن
اكتب ملفاً باسم stock.py فيه قاموس لستة منتجات على الأقل — اسم المنتج هو المفتاح، والسعر هو القيمة.
ثم:
- اطبع كل منتج وسعره في أعمدة مرتبة
- اطبع المجموع والمتوسط وأعلى سعر وأدنى سعر
- استخرج اسم المنتج الأغلى — وليس السعر فقط
- أضف منتجاً جديداً، وغيّر سعر منتج، واحذف منتجاً؛ ثم اطبع القاموس كله مرة أخرى
- اطلب بـ
.get()سعر منتج غير موجود، وأعطِ رسالة كقيمة افتراضية
ثم اكتب برنامجاً للعدّ: ضع بعض الكلمات في قائمة (بعضها مكرر)، واستخدم النمط counts.get(word, 0) + 1 لتعدّ كم مرة تظهر كل كلمة واطبع النتيجة.
في النهاية اكسر البرنامج عمداً: اطلب مفتاحاً غير موجود بالأقواس المربعة، وحاول جعل قائمة مفتاحاً. سيظهر خطآن مختلفان — اشرح لنفسك سبب كل منهما.
الخطوة ٦ من ٦
التحدي — اختبار الفصل
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
سجّل الدخول لأداء الاختبار