القواميس — أزواج المفاتيح والقيم
تخزين القيم وجلبها بالاسم، تجنب KeyError باستخدام .get()، إضافة المفاتيح وتعديلها وحذفها، التكرار باستخدام .items()، ولماذا لا يمكن للقائمة أن تكون مفتاحاً.
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
نريد تخزين درجات ثلاثة طلاب — مع تحديد درجة كل طالب بدقة.
توفر لنا القوائم طريقة واحدة فقط للقيام بذلك: قائمتان متوازيتان جنباً إلى جنب.
names = ["rafi", "ahmed", "bilal"]
marks = [72, 45, 90]
print(names[1], marks[1])ahmed 45هذا الكود يعمل، لكن هذا الترتيب بالكامل يعتمد فقط على انضباطك الشخصي كمبرمج، دون أي مساعدة أو ضمانة من بايثون.
ماذا لو أضفت اسماً جديداً إلى names ونسيت إضافة درجته إلى marks؟ ستصبح القائمتان بطولين مختلفين، ومن تلك اللحظة فصاعداً سيشير names[2] و marks[2] إلى شخصين مختلفين تماماً! ولن تعترض بايثون أو تنبهك بأي خطأ. وماذا لو رتّبت قائمة names؟ ستنهار العلاقة بين الأسماء والدرجات تماماً وفي صمت تام!
وهنا يكمن أصل المشكلة: العلاقة بين الشيئين غير مسجلة في أي مكان في الكود. العلاقة مبنية فقط على افتراض "وجودهما في نفس الفهرس" — وهي مجرد صدفة هشة معرضة للانهيار في أي لحظة.
يأتي القاموس (Dictionary) ليكتب هذه العلاقة مباشرة في صلب الكود: أسماء صريحة بدلاً من الفهارس الرقمية، والزوج (المفتاح والقيمة) محفوظان معاً في مكان واحد آمن.
في نهاية هذا الدرس ستكون قادراً على
- إنشاء قاموس واسترجاع القيم باستخدام المفاتيح
- تجنب خطأ
KeyErrorباستخدام دالة.get()ومعرفة متى تستخدم كلاً منهما - إضافة المفاتيح وتعديلها وحذفها
- التنقل والتكرار عبر القاموس باستخدام
.keys()و.values()و.items() - شرح لماذا لا يمكن للقائمة أن تكون مفتاحاً بينما يمكن للصف (tuple) أن يكون كذلك
المتطلبات السابقة: الصفوف وفك الحزم — Tuples and 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 ودالة .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
Falseالعدد 72 موجود بالتأكيد كقيمة، لكن المعامل 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() صفاً (tuple) في كل دورة — وتقوم العبارة 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ما الذي يصلح لأن يكون مفتاحاً؟
لكي يصلح أي كائن ليكون مفتاحاً، يجب أن يكون غير قابل للتعديل (immutable). النصوص والأرقام والصفوف كلها تصلح تماماً. أما القوائم فلا تصلح:
d = {}
d[["a", "b"]] = 1TypeError: unhashable type: 'list'كلمة "unhashable" تستحق التوضيح: يعثر القاموس على العناصر بسرعة هائلة لأنه يقوم بحساب رقم خاص (hash) لكل مفتاح، ويستخدم هذا الرقم للذهاب مباشرة إلى المكان الصحيح في الذاكرة. وإذا كان المفتاح قابلاً للتعديل بعد إنشائه، فإن هذا الرقم سيتغير وسيضيع العنصر داخل الذاكرة ولا يمكن الوصول إليه. لذلك ترفض بايثون بشكل قاطع استخدام أي كائن قابل للتعديل كمفتاح.
أما الصف (tuple) فلا يمكن تعديله، مما يجعله مفتاحاً ممتازاً:
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" هي طريقة لكتابة الشرط في سطر واحد، وتُقرأ من المنتصف: ""pass" إذا كان mark >= 40، وإلا فـ "fail"". جملة if/else المكونة من أربعة أسطر في الدرس التاسع تُستخدم هنا لاختيار قيمة واحدة، ولذلك يكفي سطر واحد. اكتب جملة if كاملة للقرارات المعقدة — أما هذه الصيغة المختصرة فمخصصة فقط للاختيار السريع بين قيمتين.
max(marks, key=marks.get) يوضح وسيط key= من الدرس الثالث عشر وهو يؤدي مهمة جديدة. كتابة 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) بدلاً منها — التعبير tuple(my_list) ينشئ صفاً منها.
الحلقة for x in my_dict لا تعطيني القيم وهذا هو التصرف الصحيح — فهذه الحلقة تدور على المفاتيح فقط. للحصول على القيم استخدم .values()، وللحصول على كليهما استخدم .items().
تشابه مفتاحان فاختفى أحد السجلات المفتاح لا يمكن أن يوجد إلا مرة واحدة، والمفتاح اللاحق استبدل السابق في صمت. للاحتفاظ بعدة سجلات تحت اسم واحد، اجعل القيمة عبارة عن قائمة — وهو موضوع الدرس السابع عشر.
TypeError: 'dict' object is not subscriptable، أو KeyError: 0 محاولة كتابة my_dict[0]. القاموس لا يتعامل بالفهارس الرقمية الموضعية؛ وسيتم البحث عن 0 كمفتاح بالاسم.
دالة .sort() لا تعمل على القاموس هذا صحيح — فالقواميس لا تملك دالة .sort(). استدعاء sorted(d.items()) يعيد قائمة مرتبة من الأزواج، وهو ما تحتاجه عادة.
Step 4 of 6 — Predict
Check your understanding
A loop runs directly over a dictionary. What is printed?
marks = {"rafi": 72, "ahmed": 45}
for name in marks:
print(name)- Arafi ahmed
- B72 45
- Crafi 72 ahmed 45
- D('rafi', 72) ('ahmed', 45)
A key that is not there is asked for. What happens, and what is the safe alternative?
marks = {"rafi": 72}
print(marks["dia"])- AA `KeyError` — the safe alternative is `marks.get("dia")`, which gives `None`
- BIt prints `None`
- CAn `IndexError`
- DIt prints a blank line
The counting pattern. What is printed?
words = ["pen", "bag", "pen"]
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts)- A{'pen': 2, 'bag': 1}
- B{'pen': 1, 'bag': 1}
- C{'pen': 3, 'bag': 1}
- DA `KeyError`
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
دورك الآن
اكتب ملفاً باسم stock.py يحتوي على قاموس يضم ستة منتجات على الأقل — اسم المنتج هو المفتاح، وسعره هو القيمة.
ثم قم بما يلي:
- اطبع كل منتج وسعره في أعمدة منسقة ومحاذية
- اطبع المجموع الإجمالي، والمتوسط، وأعلى سعر، وأدنى سعر
- اعثر على اسم المنتج الأغلى سعراً، وليس فقط قيمته
- أضف منتجاً جديداً، وعدّل سعر منتج موجود، واحذف منتجاً، ثم اطبع القاموس كاملاً مرة أخرى
- استخدم
.get()للبحث عن منتج غير موجود، مع وضع رسالة توضيحية كقيمة افتراضية
ثم اكتب برنامج إحصاء: ضع عدة كلمات في قائمة، مع تكرار بعضها، واحسب عدد مرات ظهور كل كلمة باستخدام نمط counts.get(word, 0) + 1.
وأخيراً تسبب في خطأين عمداً: اطلب مفتاحاً غير موجود باستخدام الأقواس المربعة، وحاول استخدام قائمة كمفتاح في القاموس. ستحصل على خطأين مختلفين — اشرح لنفسك الفرق بينهما ولماذا حدث كل منهما.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz