تخيل معايا: صاحبك المطور فتح الكود بتاعك ولقى القاموس (dictionary) ده:
{'Apple': 1, 'Apple': 2, 'Apple': 3}
ردة فعله: 😱 "إيه ده؟! إزاي فيه 3 مفاتيح بنفس الاسم؟!"
المفروض: في بايثون، مفاتيح القواميس (dictionary keys) لازم تكون فريدة (unique)!
السؤال: إزاي عملت كده؟! 🤔
ده موضوع المقال - بس الأول لازم نفهم إزاي القواميس ومفاتيحها بتشتغل من جوا!
في بايثون: القاموس = مجموعة أزواج من (مفتاح → قيمة)
مثال عملي (قاعدة بيانات مبسطة):
users = {
'[email protected]': {'name': 'أحمد', 'age': 28},
'[email protected]': {'name': 'سارة', 'age': 25},
'[email protected]': {'name': 'خالد', 'age': 32}
}
الميزة الكبيرة: البحث سريع جداً!
users['[email protected]'] - فوراً!يعني إيه "سريع"؟
السر: القواميس بتستخدم Hash Tables - تقنية ذكية بتخلي البحث سريع!
يعني إيه Hashable؟
دالة الـ Hash:
مثال:
hash('Apple') # النتيجة: رقم ثابت مثلاً -5693923834089884903
hash('Apple') # نفس الرقم دايماً!
hash('Orange') # رقم مختلف
الفكرة: بايثون بتستخدم الـ hash عشان تلاقي المفتاح بسرعة - زي فهرس الكتاب!
تخيل: عايزين نعمل 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:
نفس القيمة → نفس الـ 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 متساويين ولا لأ!
طب نجرب نخلي الـ 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}
أحلى! 😍 دلوقتي القاموس شكله حلو ومفهوم!
دلوقتي جاهزين نحل اللغز اللي في البداية!
الفكرة الشريرة:
__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" مش نفس الحاجة - بس بيبانوا إنهم نفس الحاجة!
ده تضليل خطير! 🚨
تخيل معايا السيناريو الكارثي ده:
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
إيه اللي حصل؟!
apple للقاموس → الـ hash كان بناءً على "Apple""Banana" → الـ hash اتغير!apple → بايثون بتدور في مكان تاني (hash جديد)يعني: المفتاح موجود في القاموس، بس بايثون مش لاقياه لأن الـ hash اتغير!
عشان كده: بايثون بتشجع على استخدام مفاتيح immutable زي:
"Apple"42, 3.14(1, 2, 3)[1, 2, 3] - قابلة للتعديل!{'a': 1} - قابلة للتعديل!رحلة اليوم علمتنا:
__hash__(): بتحدد الـ hash value للـ object__eq__(): بتحدد المساواة بين objects__repr__(): بتحدد شكل الـ object لما يتطبعممكن نخلي مفاتيح مختلفة تبان إنها نفس الحاجة:
__repr__() نفس الحاجة (عشان يبانوا متشابهين)لكن: ده تضليل خطير - ممنوع استخدامه في كود حقيقي! ⚠️
لو ما عرفتش __hash__() و __eq__():
المصدر: وثائق بايثون الرسمية
المقال ده لأغراض تعليمية بس!
الهدف:
زي ما بنقول: "اعرف القواعد كويس عشان تعرف امتى وإزاي تخالفها - لو فعلاً محتاج!"
بس في الكود الحقيقي: استخدم القواميس بطريقة عادية - مفاتيح فريدة، immutable، وواضحة! ✅
ملاحظة أخيرة: أنا بحب ألعب بحدود اللغة وأشوف إزاي الحاجات بتشتغل لما "أثنيها" شوية - وأظن إنت كمان! 😄
بس دايماً افتكر: الكود اللي بتكتبه هيقراه حد تاني - ما تضللوش! 😉
طارق عمرو، 12 يناير 2023