القواميس في Python (dict): الإنشاء والطرق والتكرار والترتيب وfrozendict
القاموس (dict) في Python مجموعة قابلة للتعديل من أزواج «مفتاح ـ قيمة»، ويجد القيمة بالمفتاح تقريباً فوراً، ويُنشأ بالأقواس المعقوفة مثل {"قهوة": 250} أو بالدالة dict(). المفاتيح فريدة ويجب أن تكون قابلة للتجزئة، والطريقة get تقرأ القيمة دون أن تسبب KeyError، وتُرتَّب الأزواج بالدالة sorted لأن القاموس لا يملك طريقة sort.
القاموس في Python (dict) مجموعة من أزواج «مفتاح ـ قيمة»، وتُقرأ القيمة بالمفتاح تقريباً في لحظة مهما كان عدد السجلات. إذا أردت حساب المصروفات حسب الفئة، أو حفظ الإعدادات، أو البحث عن مستخدم بمعرّفه بسرعة، فالقاموس هو الأداة المناسبة. وفيما يلي كل ما تحتاجه للعمل مع python dict: الإنشاء، والطرق، والتكرار، والترتيب، والدمج، والجديد في الإصدار 3.15 وهو frozendict.
ما هو القاموس في Python
يحفظ القاموس أزواجاً مفاتيحها فريدة: إذا كتبت قيمة لمفتاح موجود، تُستبدل القيمة القديمة. داخلياً هو جدول تجزئة (hash table)، لذلك تتم عمليات البحث والإضافة والحذف بالمفتاح في زمن ثابت في المتوسط، ولا تحتاج إلى المرور على كل العناصر.
المفتاح يجب أن يكون كائناً قابلاً للتجزئة: نص أو رقم أو صف من قيم غير قابلة للتعديل. ولا يصلح القائمة مفتاحاً، وستحصل على TypeError: unhashable type: 'list'. أما القيم فيمكن أن تكون أي شيء، بما في ذلك قواميس وقوائم أخرى.
منذ Python 3.7 يحفظ القاموس ترتيب الإدخال بضمان: عند المرور عليه تحصل على المفاتيح بالترتيب الذي أضفتها به.
كيف تنشئ قاموساً
هناك عدة طرق لإنشاء قاموس، وكلها تظهر في الكود الحقيقي.
spent = {"طعام": 1200, "تاكسي": 450} # صيغة الأقواس المعقوفة
empty = {} # قاموس فارغ
also_empty = dict() # الشيء نفسه بالدالة dict
user = dict(name="أمينة", age=30) # بالمعاملات المسماة
pairs = dict([("a", 1), ("b", 2)]) # من قائمة أزواج
keys = dict.fromkeys(["x", "y"], 0) # {'x': 0, 'y': 0}
انتبه إلى أن الأقواس المعقوفة الفارغة {} تنشئ قاموساً فارغاً لا مجموعة. المجموعة الفارغة تُنشأ بـ set() فقط.
وطريقة مفيدة لبناء قاموس من قائمتين هي zip:
names = ["طعام", "تاكسي", "سينما"]
sums = [1200, 450, 600]
spent = dict(zip(names, sums))
print(spent) # {'طعام': 1200, 'تاكسي': 450, 'سينما': 600}
الوصول بالمفتاح: d[k] وget
تُقرأ القيمة بالأقواس المربعة. إذا لم يوجد المفتاح يرفع Python الخطأ KeyError. ولتجنب هذا الخطأ استخدم الطريقة get: تعيد None أو القيمة الافتراضية التي تحددها.
spent = {"طعام": 1200}
print(spent["طعام"]) # 1200
print(spent.get("سينما")) # None
print(spent.get("سينما", 0)) # 0
spent["سينما"] # KeyError: 'سينما'
للتحقق من وجود مفتاح في القاموس استخدم العامل in: "طعام" in spent تعيد True. وهو يتحقق من المفاتيح فقط، لا من القيم. للتحقق من القيم اكتب 1200 in spent.values().
الإضافة والتعديل والحذف
إضافة سجل أو تحديثه سطر واحد بالإسناد. ومع العدادات يكون الجمع مع get مفيداً جداً:
spent = {}
for category, amount in [("طعام", 250), ("تاكسي", 300), ("طعام", 400)]:
spent[category] = spent.get(category, 0) + amount
print(spent) # {'طعام': 650, 'تاكسي': 300}
يُحذف المفتاح بـ del d[k] (وترفع KeyError إن لم يكن موجوداً)، أو بـ d.pop(k, default) التي تعيد القيمة أيضاً. ولتفريغ القاموس كله استخدم d.clear(). وعدد الأزواج هو len(d).
طرق القواميس: ورقة مرجعية
| الطريقة | ماذا تفعل |
|---|---|
get(k, default) | القيمة عند المفتاح أو default دون خطأ |
setdefault(k, default) | تعيد القيمة؛ وإذا لم يوجد المفتاح تكتب default أولاً |
update(other) | تضيف الأزواج من قاموس آخر وتستبدل الموجود منها |
pop(k, default) | تحذف المفتاح وتعيد قيمته |
popitem() | تحذف آخر زوج أُضيف وتعيده |
keys()، values()، items() | عروض للمفاتيح والقيم والأزواج |
copy() | نسخة سطحية |
clear() | تفريغ القاموس |
fromkeys(keys, value) | قاموس جديد بالقيمة نفسها لكل المفاتيح |
setdefault: ملء القاموس بالقوائم
تنقذك الطريقة setdefault عندما تكون القيم قوائم وتحتاج إلى التجميع:
groups = {}
for item, cat in [("قهوة", "طعام"), ("مترو", "مواصلات"), ("بيتزا", "طعام")]:
groups.setdefault(cat, []).append(item)
print(groups) # {'طعام': ['قهوة', 'بيتزا'], 'مواصلات': ['مترو']}
update: تحديث القاموس
تعدّل update القاموس في مكانه: تُضاف المفاتيح الجديدة، وتُستبدل المفاتيح المتطابقة.
cfg = {"debug": False, "lang": "ar"}
cfg.update({"debug": True, "theme": "dark"})
print(cfg) # {'debug': True, 'lang': 'ar', 'theme': 'dark'}
defaultdict: قاموس بقيمة افتراضية
collections.defaultdict نوع فرعي من dict ينشئ القيمة بنفسه عند الوصول إلى مفتاح غير موجود، باستخدام الدالة التي تحددها. فيقصر كود العدّ والتجميع.
from collections import defaultdict
spent = defaultdict(int)
for cat, amount in [("طعام", 250), ("تاكسي", 300), ("طعام", 400)]:
spent[cat] += amount
print(dict(spent)) # {'طعام': 650, 'تاكسي': 300}
groups = defaultdict(list)
groups["طعام"].append("قهوة")
للعدّ البسيط هناك أيضاً collections.Counter: Counter(["a", "b", "a"]) تعطي Counter({'a': 2, 'b': 1}).
التكرار على القاموس: حلقة for
المرور على القاموس في حلقة for يكون على المفاتيح افتراضياً. ولتحصل على المفتاح والقيمة معاً مرّ على items()، وهذا هو الخيار الأكثر شيوعاً.
spent = {"طعام": 650, "تاكسي": 300}
for cat in spent: # المفاتيح
print(cat)
for amount in spent.values(): # القيم
print(amount)
for cat, amount in spent.items(): # الأزواج
print(f"{cat}: {amount} ريال")
لا يجوز تغيير حجم القاموس أثناء المرور عليه، فسيظهر RuntimeError: dictionary changed size during iteration. وإذا أردت حذف بعض المفاتيح فمرّ على نسخة: for k in list(d):.
عرض القاموس على الشاشة
print(d) تطبع القاموس في سطر واحد. أما القاموس الكبير أو المتداخل فيُعرض بشكل أوضح بالوحدة pprint أو بـ json.dumps(d, ensure_ascii=False, indent=2)، فتظهر البيانات مع المسافات البادئة والحروف العربية دون ترميز.
ترتيب القاموس
القاموس نفسه لا يُرتَّب في مكانه، فليس له طريقة sort. ترتيب القاموس يعني إنشاء قاموس جديد من الأزواج المرتبة عبر sorted.
spent = {"تاكسي": 300, "طعام": 650, "سينما": 120}
by_key = dict(sorted(spent.items()))
print(by_key) # {'سينما': 120, 'طعام': 650, 'تاكسي': 300}
by_value = dict(sorted(spent.items(), key=lambda p: p[1], reverse=True))
print(by_value) # {'طعام': 650, 'تاكسي': 300, 'سينما': 120}
إذا كنت تريد أول N فئات فقط، فلا تحوّل النتيجة إلى قاموس: sorted(spent.items(), key=lambda p: p[1], reverse=True)[:2] تعيد قائمة من الأزواج.
ويمكن ترتيب قائمة من القواميس حسب حقل فيها: sorted(users, key=lambda u: u["age"]).
دمج القواميس وجمعها
منذ Python 3.9 تُدمج القواميس بالعامل |، والعامل |= يحدّث القاموس في مكانه. وعند تطابق المفاتيح يفوز المعامل الأيمن.
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 3}
print(a | b) # {'x': 1, 'y': 20, 'z': 3}
print({**a, **b}) # الشيء نفسه بفك التعبئة
a |= b # مثل a.update(b)
انتبه إلى أن a + b لا تعمل مع القواميس، وسيظهر TypeError. وإذا أردت فعلاً جمع القيم عند المفاتيح المتطابقة فاستخدم Counter: Counter(a) + Counter(b).
فك تعبئة القاموس
النجمتان ** تفكّان القاموس إلى معاملات مسماة لدالة، أو إلى قاموس آخر:
def greet(name, city):
return f"{name} من {city}"
data = {"name": "أمينة", "city": "الرياض"}
print(greet(**data)) # أمينة من الرياض
توليد القواميس (dict comprehension)
يبني مولّد القواميس قاموساً في سطر واحد من أي متتالية:
squares = {n: n * n for n in range(5)}
print(squares) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
big = {k: v for k, v in spent.items() if v > 200}
inverted = {v: k for k, v in {"a": 1, "b": 2}.items()}
print(inverted) # {1: 'a', 2: 'b'}
المثال الأخير يوضح كيف تقلب القاموس بتبديل المفاتيح والقيم. ويعمل هذا إذا كانت القيم فريدة وقابلة للتجزئة، وإلا فستضيع بعض الأزواج.
القواميس المتداخلة
يمكن أن تكون القيمة قاموساً آخر، وبهذا تُوصف بنى مثل ردود الـ API بصيغة JSON، أو قاموس من قواميس المستخدمين.
users = {
101: {"name": "أمينة", "skills": ["python", "sql"]},
102: {"name": "عمر", "skills": ["go"]},
}
print(users[101]["skills"][0]) # python
print(users.get(103, {}).get("name")) # None، دون KeyError
نسخ القاموس: النسخة والاسم الثاني
التعبير b = a لا ينسخ القاموس، بل يضع اسماً ثانياً على الكائن نفسه. إذا عدّلت عبر b فستظهر التغييرات في a أيضاً. والأمر نفسه عند تمرير القاموس إلى دالة، فهي تحصل على مرجع للكائن المشترك وقد تفسده دون أن تشعر.
import copy
a = {"limits": [0, 500]}
b = a.copy() # نسخة سطحية
b["limits"].append(1000) # القائمة الداخلية مشتركة!
print(a) # {'limits': [0, 500, 1000]}
c = copy.deepcopy(a) # نسخة مستقلة تماماً
مقارنة القواميس
العامل == يقارن القواميس بالمحتوى: تكون متساوية إذا تطابقت كل الأزواج. ترتيب المفاتيح لا يدخل في الحساب: {"a": 1, "b": 2} == {"b": 2, "a": 1} تعطي True. ولإيجاد المفاتيح المشتركة استخدم عمليات المجموعات على العروض: a.keys() & b.keys().
قاموس بأنواع محددة: التلميحات
يُكتب تلميح القاموس بالشكل dict[str, int]. وإذا كان للقاموس حقول ثابتة بأنواع مختلفة فيفيد TypedDict من الوحدة typing:
from typing import TypedDict
class User(TypedDict):
name: str
age: int
u: User = {"name": "أمينة", "age": 30}
التلميحات لا تُفحص أثناء التشغيل، بل تقرأها بيئات التطوير ومحللات مثل mypy.
القاموس غير القابل للتعديل: frozendict في Python 3.15
ظهر في Python 3.15 نوع مدمج اسمه frozendict (PEP 814): لا يحتاج إلى استيراد، ومحاولة تعديله تنتهي فوراً بـ TypeError. وقبل ذلك كان القاموس غير القابل للتعديل يُحاكى بغلاف من الوحدة types أو بحزمة خارجية. وهذا بالضبط ما تحتاجه للإعدادات المشتركة التي لا ينبغي لأي دالة أن تغيّرها بصمت.
config = frozendict(min_amount=0, max_amount=500)
config["max_amount"] = 1000
# TypeError: 'frozendict' object does not support item assignment
strict = config | {"max_amount": 300} # frozendict جديد، والأصلي سليم
cache = {config: "تقرير مايو"} # قابل للتجزئة، لذلك يصلح مفتاحاً
ما يجب معرفته، وفق مادة الدورة:
frozendictيرث منobjectلا منdict، لذلك تعيدisinstance(cfg, dict)القيمةFalse. للتحقق من «أي قاموس» استخدمcollections.abc.Mappingأوisinstance(x, (dict, frozendict)).- هو قابل للتجزئة فقط إذا كانت مفاتيحه وقيمه قابلة للتجزئة:
frozendict(a=[1])يُنشأ، لكنhash()يفشل. وعدم القابلية للتعديل سطحي، كما في الصف. - يحفظ الترتيب، لكن
==لا يأخذه في الحساب. - تفهم
json.dumpsوpickleوcopyنوع frozendict.
أخطاء شائعة
- KeyError عند القراءة: استخدم
getأو تحقق بالعاملinأولاً. - قائمة مفتاحاً: حوّلها إلى صف:
d[tuple(lst)]. - تعديل القاموس داخل حلقة: مرّ على
list(d). - توقع نسخة من الإسناد: استخدم
copy()أوdeepcopy. - كتابة
{}بدلاً من مجموعة فارغة: الناتج قاموس فارغ.
الخلاصة
قاموس Python (dict) هو بنية البيانات الأهم في اللغة: بحث سريع بالمفتاح، وحفظ لترتيب الإدخال، وطرق مفيدة مثل get وsetdefault وupdate، والمرور عبر items()، والترتيب بـ sorted، والدمج بالعامل |. تذكّر أمر النسخ والمفاتيح القابلة للتجزئة، وللإعدادات في Python 3.15 استخدم frozendict. وقد شُرحت القواميس والقوائم والصفوف والمجموعات عملياً في دورة «بايثون 3.15: من المبتدئ إلى المحترف»، من خلال متتبّع المصروفات.
الأسئلة الشائعة
هل يمكن أن تحتوي القواميس على قيم من أنواع مختلفة؟
نعم، القيم يمكن أن تكون مختلفة في القاموس الواحد: أرقام ونصوص وقوائم وقواميس أخرى. القيد الوحيد يخص المفاتيح، فيجب أن تكون قابلة للتجزئة.
كيف أعرف عدد العناصر في القاموس؟
الدالة len(d) تعيد عدد أزواج «مفتاح ـ قيمة». وللقاموس الفارغ تعيد 0، والقاموس الفارغ في الشرط if يُعامَل كقيمة خاطئة.
ما الفرق بين dict وdefaultdict؟
defaultdict نوع فرعي من dict، لا يرفع KeyError عند الوصول إلى مفتاح غير موجود، بل ينشئ القيمة بدالة مثل int أو list. وفي بقية السلوك يتصرف كقاموس عادي.
كيف أحصل على المفتاح من القيمة؟
لا توجد طريقة مباشرة، فيلزم المرور على العناصر: next((k for k, v in d.items() if v == value), None). وإذا كان البحث متكرراً فالأسهل بناء قاموس عكسي بتوليد {v: k for k, v in d.items()}.
كيف أحفظ القاموس في ملف؟
غالباً عبر الوحدة json: تكتب json.dump(d, f, ensure_ascii=False, indent=2) إلى ملف مفتوح للكتابة، وتقرأ بـ json.load(f). وتصبح المفاتيح نصوصاً بعد الحفظ.
يُشرح هذا الموضوع بالتفصيل مع التطبيق العملي في دورة «Python 3.15: от новичка до профи».
الانتقال إلى الدورة