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

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

تخيل معايا: صاحبك المطور فتح الكود بتاعك ولقى القاموس (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}
}

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

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

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

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

يعني إيه Hashable؟

دالة الـ 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 ساذجة بتجمع أحرف الكلمة:

الحالة دي اسمها 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}

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

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

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

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}

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

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

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

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

الكود:

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 زي:

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

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

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

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

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

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

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

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

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

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

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

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

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

الهدف:

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

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


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

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


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

الترجمات: [EN], [NL]