tarekamr.com — إزاي تضلل زمايلك المطورين ويبقى عندك قواميس بايثون بمفاتيح مش فريدة!
~ / pages / إزاي تضلل زمايلك المطورين ويبقى عندك قواميس بايثون بمفاتيح مش فريدة!

إزاي تضلل زمايلك المطورين ويبقى عندك قواميس بايثون بمفاتيح مش فريدة!

افهم إزاي كائنات بايثون بتتصرف كمفاتيح في القواميس

تخيل معايا: صاحبك المطور فتح الكود بتاعك ولقى القاموس (dictionary) ده:

{'Apple': 1, 'Apple': 2, 'Apple': 3}

ردة فعله: 😱 "إيه ده؟! إزاي فيه 3 مفاتيح بنفس الاسم؟!"

المفروض: في بايثون، مفاتيح القواميس (dictionary keys) لازم تكون فريدة (unique)!

السؤال: إزاي عملت كده؟! 🤔

ده موضوع المقال - بس الأول لازم نفهم إزاي القواميس ومفاتيحها بتشتغل من جوا!

إيه هي القواميس (Dictionaries) أصلاً؟

في بايثون: القاموس = مجموعة أزواج من (مفتاح → قيمة)

مثال عملي (قاعدة بيانات مبسطة):

users = {
    '[email protected]': {'name': 'أحمد', 'age': 28},
    '[email protected]': {'name': 'سارة', 'age': 25},
    '[email protected]': {'name': 'خالد', 'age': 32}
}

الميزة الكبيرة: البحث سريع جداً!

  • لو عايز بيانات أحمد → users['[email protected]'] - فوراً!
  • مش محتاج تدور في كل العناصر واحد واحد

يعني إيه "سريع"؟

  • القائمة العادية (list): لو فيها مليون عنصر، ممكن تدور في كلهم! ⏱️ بطيء
  • القاموس (dict): حتى لو فيه مليون عنصر، البحث فوري تقريباً!سريع

السر: القواميس بتستخدم Hash Tables - تقنية ذكية بتخلي البحث سريع!

القاعدة الأولى: المفاتيح لازم تكون "قابلة للهاش" (Hashable)

يعني إيه Hashable؟

دالة الـ Hash:

  • بتاخد أي قيمة (نص، رقم، object)
  • بترجع رقم ثابت (hash value)
  • نفس القيمةنفس الـ hash دايماً

مثال:

hash('Apple')   # النتيجة: رقم ثابت مثلاً -5693923834089884903
hash('Apple')   # نفس الرقم دايماً!
hash('Orange')  # رقم مختلف

الفكرة: بايثون بتستخدم الـ hash عشان تلاقي المفتاح بسرعة - زي فهرس الكتاب!

نجرب نعمل Class خاص بينا

تخيل: عايزين نعمل class اسمه Fruit ونستخدم objects منه كمفاتيح قاموس!

class Fruit:

    def __hash__(self):
        return 1  # كل الفواكه ليها نفس الـ hash!

d = {
    Fruit(): 1,
    Fruit(): 2
}

# لما نطبع d:
# {<__main__.Fruit at 0x10873eb10>: 1, <__main__.Fruit at 0x10873d9d0>: 2}

لاحظ: رغم إن الـ hash نفس الحاجة (كله 1)، القاموس عنده مفتاحين منفصلين!

ليه؟ 🤔

التصادم (Hash Collision): لما رقمين مختلفين يطلعوا نفس الـ Hash!

القاعدة المهمة في دوال الـ Hash:

نفس القيمة → نفس الـ hash دايماً ✅ لكن نفس الـ hash ← مش معناه نفس القيمة! ❌

يعني إيه؟

تخيل: دالة hash ساذجة بتجمع أحرف الكلمة:

  • "ABC" → A=1, B=2, C=3 → المجموع = 6
  • "BAC" → B=2, A=1, C=3 → المجموع = 6 (نفس الـ hash!)
  • لكن "ABC" مش "BAC" - كلمات مختلفة!

الحالة دي اسمها Hash Collision (التصادم)!

عشان كده: بايثون مش بتكتفي بالـ hash - بتتأكد كمان إن الـ objects متساويين ولا لأ!

القاعدة التانية: المفاتيح لازم تكون فريدة (Unique)

طب نجرب نخلي الـ objects متساوية:

class Fruit:

    def __hash__(self):
        return 1  # نفس الـ hash

    def __eq__(self, other):
        return True  # كل الفواكه متساوية!

d = {
    Fruit(): 1,
    Fruit(): 2
}

# لما نطبع d:
# {<__main__.Fruit at 0x10873ed50>: 2}

تادا! 🎉 دلوقتي القاموس شايف إن الاتنين نفس المفتاح!

  • الـ hash متساوي
  • الـ objects متساوية
  • النتيجة: المفتاح التاني بيستبدل الأول!

نتحكم في المساواة برمجياً

طبعاً مش كل الفواكه لازم تكون متساوية - نقدر نقرر بناءً على الاسم:

class Fruit:

    def __init__(self, name):
        self.name = name

    def __hash__(self):
        return hash(self.name)  # الـ hash بناءً على الاسم

    def __eq__(self, other):
        return self.name == other.name  # متساويين لو نفس الاسم

d = {
    Fruit("Apple"): 1,
    Fruit("Orange"): 2,
    Fruit("Apple"): 3  # دي هتستبدل الأولى!
}

# النتيجة:
# {<__main__.Fruit at 0x10873cb90>: 3, <__main__.Fruit at 0x108247ad0>: 2}

منطقي! Apple التانية استبدلت الأولى، لكن Orange لسه موجودة!

المشكلة: شكل المفاتيح وحش! 😖

الـ output بيطلع كده:

{<__main__.Fruit at 0x10873cb90>: 3, ...}

مش عايزين الـ <__main__.Fruit at ... القبيحة دي!

الحل: نستخدم الـ __repr__() method!

class Fruit:

    def __init__(self, name):
        self.name = name

    def __hash__(self):
        return hash(self.name)

    def __eq__(self, other):
        return self.name == other.name

    def __repr__(self):
        return f"'{self.name}'"  # كده هيطلع حلو!

d = {
    Fruit("Apple"): 1,
    Fruit("Orange"): 2,
    Fruit("Apple"): 3
}

# دلوقتي النتيجة:
# {'Apple': 3, 'Orange': 2}

أحلى! 😍 دلوقتي القاموس شكله حلو ومفهوم!

حل اللغز: إزاي نعمل قاموس بمفاتيح "مش فريدة"؟ 😈

دلوقتي جاهزين نحل اللغز اللي في البداية!

الفكرة الشريرة:

  • نخلي الـ hashes مختلفة → عشان بايثون تشوفهم مفاتيح منفصلة
  • بس الـ __repr__() يطلع نفس الحاجة → عشان يبان إنهم نفس المفتاح!

الكود:

class Fruit:

    def __init__(self, name):
        self.name = name

    def __hash__(self):
        return hash(self.name)  # كل فاكهة ليها hash مختلف

    def __eq__(self, other):
        return self.name == other.name

    def __repr__(self):
        return "'Apple'"  # لكن كلهم بيطلعوا "Apple" في العرض!

d = {
    Fruit("Apple"): 1,
    Fruit("Orange"): 2,   # hash مختلف!
    Fruit("Banana"): 3    # hash مختلف!
}

# النتيجة المضللة:
print(d)
# {'Apple': 1, 'Apple': 2, 'Apple': 3}

🎭 الخدعة اشتغلت! القاموس شكله فيه 3 مفاتيح بنفس الاسم!

ليه ده شرير؟ ⚠️

المشكلة: أي حد يشوف الكود ده هيتضلل!

المشكلة الأولى: مش هيقدر يوصل للقيم!

d['Apple']  # ❌ KeyError!

ليه؟ لأن 'Apple' (string) مش Fruit('Apple') (object) - الـ hash مختلف!

المشكلة التانية: المفاتيح بتبان زي بعض بس مش زي بعض!

list(d.keys())
# ['Apple', 'Apple', 'Apple']  # شكلهم زي بعض

# لكن لو جربت:
key1, key2, key3 = d.keys()
key1 == key2  # False! - مش متساويين فعلاً!

يعني: الـ 3 "Apple" مش نفس الحاجة - بس بيبانوا إنهم نفس الحاجة!

ده تضليل خطير! 🚨

ليه بايثون عايزة المفاتيح Immutable (غير قابلة للتغيير)؟

تخيل معايا السيناريو الكارثي ده:

class Fruit:
    def __init__(self, name):
        self.name = name

    def __hash__(self):
        return hash(self.name)

    def __eq__(self, other):
        return self.name == other.name

    def __repr__(self):
        return self.name

apple = Fruit("Apple")
orange = Fruit("Orange")

d = {
    apple: 1,
    orange: 2
}

print(d)  # {Apple: 1, Orange: 2} ✅ تمام

دلوقتي نغير اسم Apple:

apple.name = "Banana"  # 😱 غيرنا الاسم!

print(d)  # {Banana: 1, Orange: 2}  # الشكل اتغير

# طب نحاول نوصل للقيمة:
d[apple]  # ❌ KeyError: Banana

إيه اللي حصل؟!

  1. لما أضفنا apple للقاموس → الـ hash كان بناءً على "Apple"
  2. بايثون خزنت المفتاح في مكان معين بناءً على الـ hash ده
  3. لما غيرنا الاسم لـ "Banana" → الـ hash اتغير!
  4. لما بندور على apple → بايثون بتدور في مكان تاني (hash جديد)
  5. ما بتلاقيش المفتاح → KeyError!

يعني: المفتاح موجود في القاموس، بس بايثون مش لاقياه لأن الـ hash اتغير!

عشان كده: بايثون بتشجع على استخدام مفاتيح immutable زي:

  • ✅ Strings: "Apple"
  • ✅ Numbers: 42, 3.14
  • ✅ Tuples: (1, 2, 3)
  • ❌ Lists: [1, 2, 3] - قابلة للتعديل!
  • ❌ Dictionaries: {'a': 1} - قابلة للتعديل!

الخلاصة: فهمنا إيه النهارده؟

رحلة اليوم علمتنا:

1. إزاي القواميس بتشتغل من جوا

  • Hash Function: بتحول المفتاح لرقم عشان البحث يبقى سريع
  • Hash Collision: ممكن مفاتيح مختلفة يكون ليها نفس الـ hash
  • Equality Check: بايثون بتتأكد من الـ hash والمساواة مع بعض

2. الـ Magic Methods المهمة

  • __hash__(): بتحدد الـ hash value للـ object
  • __eq__(): بتحدد المساواة بين objects
  • __repr__(): بتحدد شكل الـ object لما يتطبع

3. الخدعة الشريرة 😈

ممكن نخلي مفاتيح مختلفة تبان إنها نفس الحاجة:

  • نخلي الـ hash مختلف (عشان بايثون تشوفهم منفصلين)
  • نخلي الـ __repr__() نفس الحاجة (عشان يبانوا متشابهين)

لكن: ده تضليل خطير - ممنوع استخدامه في كود حقيقي! ⚠️

4. ليه المفاتيح لازم تكون Immutable

  • لو غيرت المفتاح بعد إضافته → الـ hash بيتغير
  • بايثون مش هتلاقي المفتاح تاني (بتدور في المكان الغلط!)
  • النتيجة: KeyError و bugs صعبة الاكتشاف

5. Default Behavior في بايثون

لو ما عرفتش __hash__() و __eq__():

  • بايثون بتديك تطبيق افتراضي
  • كل object مختلف عن أي object تاني
  • بس الـ object يساوي نفسه بس

المصدر: وثائق بايثون الرسمية

الدرس المستفاد 🎓

المقال ده لأغراض تعليمية بس!

الهدف:

  • ✅ نفهم إزاي بايثون بتشتغل من جوا
  • ✅ نعرف ليه بايثون بتفرض قواعد معينة
  • ✅ نتعلم من ثني القواعد - بس ما نكسرهاش في كود حقيقي!

زي ما بنقول: "اعرف القواعد كويس عشان تعرف امتى وإزاي تخالفها - لو فعلاً محتاج!"

بس في الكود الحقيقي: استخدم القواميس بطريقة عادية - مفاتيح فريدة، immutable، وواضحة!


ملاحظة أخيرة: أنا بحب ألعب بحدود اللغة وأشوف إزاي الحاجات بتشتغل لما "أثنيها" شوية - وأظن إنت كمان! 😄

بس دايماً افتكر: الكود اللي بتكتبه هيقراه حد تاني - ما تضللوش! 😉


طارق عمرو، 12 يناير 2023

الترجمات: [EN] [NL]   [↓ .md]
~ / pages / إزاي تضلل زمايلك المطورين ويبقى عندك قواميس بايثون بمفاتيح مش فريدة!
>