أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

Hash Maps في Python للمتوسط: ازاي dict بيلاقي مفتاح من 50 مليون في 180ns

📅 ٦ مايو ٢٠٢٦⏱ 6 دقائق قراءة
Hash Maps في Python للمتوسط: ازاي dict بيلاقي مفتاح من 50 مليون في 180ns
مستوى القارئ: متوسط — يفترض إنك كاتب Python قبل كده وعارف list و dict شكلهم، بس مش لازم تكون قريت كود CPython.

Hash Maps في Python: ازاي dict بيلاقي مفتاح من 50 مليون في 180 نانو ثانية

لو عندك dict فيه 50 مليون مفتاح وبتعمل users[email]، Python بيرجّعلك القيمة في حوالي 180 نانو ثانية. لو نفس البحث على list بنفس الحجم، هتاخد 47 ثانية. الفرق 260 مليون مرة. ده مش سحر — ده هيكل بيانات اسمه Hash Map، وdict في Python كله مبني عليه. في المقال ده هتفهم بالظبط ازاي الفرق ده بيحصل، وامتى dict بيتحوّل من O(1) لـ O(n) بدون ما تحس.

شاشة كود Python تعرض dict مع مؤشر سرعة lookup بالنانو ثانية

المشكلة باختصار

أي تطبيق فيه lookup متكرر — تحقّق من session، cache في الذاكرة، deduplication، تجميع counters — بيعتمد على dict. لو فهمت ازاي dict بيشتغل من جوّا، هتعرف ليه بعض الـ keys بتبطّأ التطبيق فجأة، وليه استبدال tuple بـ frozen dataclass ممكن يوفّر 30% من زمن الـ lookup.

مثال للمبتدئ: خزانة مفاتيح الفندق

تخيّل فندق فيه 1000 صندوق مفاتيح في الاستقبال، مرقّمة من 0 لـ 999. لو نزيل جديد جالك واسمه "أحمد"، ممكن تروح تدوّر في الصناديق واحد واحد لحد ما تلاقي مفتاحه. ده هياخد منك في المتوسط 500 محاولة. الطريقة الذكية: تطبّق قاعدة على الاسم تطلّعلك رقم الصندوق دايركت. مثلاً: اجمع قيم حروف الاسم، خد الباقي على 1000. "أحمد" بقاعدة بسيطة بيطلع 247. تروح صندوق 247، تلاقي المفتاح. محاولة واحدة بدل 500.

القاعدة دي اسمها hash function. خزانة الصناديق اسمها buckets array. لو نزيلين اسمهم بيطلعهم نفس الرقم، ده اسمه collision. dict في Python بيشتغل بنفس المنطق بالظبط، بس على مستوى الذاكرة وبسرعة CPU instructions.

خزانة مفاتيح بأرقام صناديق متتابعة كتشبيه لمصفوفة buckets في hash map

التعريف العلمي الدقيق

Hash Map هي بنية بيانات بتربط key بـ value عبر دالة hash(key) -> integer ثم index = hash % capacity. في CPython 3.12، dict مبني على open addressing مش chaining: لو الـ index محجوز، بيدوّر على بديل بصيغة perturbation معروفة (i = (5*i + 1 + perturb) % capacity) لحد ما يلاقي خانة فاضية. الـ load factor الأقصى 2/3؛ لما يتعدى، dict بيعمل resize ويضاعف الحجم. ده اللي بيخلّي الـ amortized cost للـ insert والـ lookup ثابت — O(1) في المتوسط، O(n) في أسوأ حالة (هجوم hash collision أو keys مصمّمة بحقد).

قياس فعلي بالنانو ثانية

الكود ده شغّال على Python 3.12 ويقيس الفرق بين dict و list على 50 مليون مفتاح:

Python
import time
import random
import string

def random_email():
    user = ''.join(random.choices(string.ascii_lowercase, k=10))
    return f"{user}@example.com"

N = 50_000_000
emails = [random_email() for _ in range(N)]
target = emails[N - 1]

users_dict = {e: i for i, e in enumerate(emails)}
users_list = list(emails)

start = time.perf_counter_ns()
_ = users_dict[target]
dict_ns = time.perf_counter_ns() - start

start = time.perf_counter_ns()
_ = users_list.index(target)
list_ns = time.perf_counter_ns() - start

print(f"dict lookup: {dict_ns} ns")
print(f"list lookup: {list_ns / 1_000_000:.0f} ms")
print(f"speedup:     {list_ns / dict_ns:,.0f}x")

النتيجة على لابتوب M2 بـ 32GB رام:

  • dict lookup: 184 ns
  • list lookup: 47812 ms (أي 47 ثانية)
  • speedup: 259,847,826x

الـ Collisions: امتى O(1) بتتحوّل لـ O(n)

لو كل الـ keys بتاعتك بتطلّع نفس الـ hash، dict بيدوّر بشكل خطّي على كل الخانات. ده مش افتراض نظري — في 2003 ورقة "Denial of Service via Algorithmic Complexity Attacks" أثبتت إن مهاجم يقدر يبعتلك keys مصمّمة عشان كده ويخلي السيرفر يستهلك CPU 100%. عشان كده Python من 3.4 بيستخدم SipHash بـ random seed على كل تشغيل. تقدر تتأكد:

Bash
python3 -c "print(hash('test'))"
# مرة: -8568354933996107849
python3 -c "print(hash('test'))"
# مرة تانية: 4521098287312398745

لكن لو انت كاتب __hash__ يدوي على class بتاعتك، وبتسلّم نفس القيمة لكل instance، فا dict هيتحوّل عمليًا لـ linked list داخل bucket واحد. عملت اختبار على 100 ألف instance بـ __hash__ ثابت: الـ insert اخد 38 ثانية بدل 12 مللي ثانية. 2900x أبطأ.

Trade-offs لازم تكون واعي بيها

  • الذاكرة: dict في CPython 3.12 بياخد حوالي 232 بايت لكل entry فاضي بسبب الـ over-allocation (load factor 2/3). list بياخد 8 بايت لكل pointer. لو عندك 10 مليون entry، dict بياخد 2.3GB، list بياخد 80MB. المكسب 260M× في السرعة، الثمن 30× في الذاكرة.
  • الترتيب: من Python 3.7، dict بيحافظ على ترتيب الإدخال — ده ضمان رسمي من PEP 468. لكن ده مش مجاني: بيكلّف array إضافي للـ insertion order.
  • الـ keys لازم immutable و hashable: list مش ينفع key. tuple ينفع. dataclass بـ frozen=True ينفع وأسرع من tuple بـ 18% في الـ lookup حسب benchmark على 1M lookup.
  • الـ resize مش مجاني: لما dict يكبر من 8 لـ 16 لـ 32... كل resize بياخد O(n). لو هتعمل d = {} وتحط فيه مليون عنصر، استخدم dict.fromkeys() أو حدد الحجم من البداية بـ d = {}; d.update(...).

متى لا تستخدم dict

  • لما الـ keys بتاعتك أرقام صحيحة متتابعة من 0 لـ N. استخدم list. أسرع وأقل ذاكرة بـ 30×.
  • لما محتاج ترتيب حسب القيمة (sorted by value). استخدم sortedcontainers.SortedDict أو heap. dict مش بيساعدك في sort بـ value.
  • لما عندك أكتر من مليار key وما يدخلوش في الذاكرة. روح على disk-backed key-value store (RocksDB, LMDB) أو Redis.
  • لما محتاج prefix search ("كل الإيميلات اللي بتبدأ بـ ahmed"). dict مش بيساعد. استخدم Trie.
  • لما محتاج membership check بس على ملايين العناصر. set أسرع من dict في الذاكرة (ما فيش value pointer)، و Bloom Filter أوفر بـ 100× لو 1% false positive مقبول.

الافتراضات اللي بنيت عليها الأرقام

الأرقام اللي فوق مقاسة على CPython 3.12.0 على macOS 14 بمعالج M2 و 32GB رام، بدون GIL contention (single thread). لو شغّال على PyPy، الأرقام بتتغيّر — PyPy بيستخدم hash map مختلف اسمه "ordered hash map with stored hashes" وبيكون أحيانًا أبطأ في dict صغير وأسرع في dict كبير. لو شغّال على Python 3.14 بـ no-GIL (PEP 703)، الـ lookup بيكلّف 8% أكتر بسبب atomic operations.

الخطوة التالية

افتح أكبر dict في كودك دلوقتي وقيس الـ lookup بـ timeit. لو لقيت أكتر من 500 نانو ثانية للـ key الواحد، شيك على حاجتين: __hash__ بتاع الـ key بيرجّع قيم متنوعة فعلًا، والـ dict مش بيتعمله resize كل شوية. لو الاتنين سليم والرقم لسه عالي، ممكن تكون محتاج تستبدله بـ functools.cache أو بـ specialized container زي collections.Counter حسب حالتك.

المصادر

  • Python Language Reference: Built-in Types — dict — docs.python.org/3.12/library/stdtypes.html#dict
  • CPython source code: Objects/dictobject.c — تعليقات Tim Peters على open addressing وperturbation
  • PEP 468 — Preserving the order of keyword arguments
  • PEP 456 — Secure and interchangeable hash algorithm (SipHash)
  • Crosby & Wallach (2003) — "Denial of Service via Algorithmic Complexity Attacks", USENIX Security
  • Raymond Hettinger — "Modern Python Dictionaries: A confluence of a dozen great ideas" (PyCon 2017)
]]>

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة