"مرحبًا مجددًا فريق بايثون العرب، معكم أحمد. بعد ما خلصت من مشكلة الـ NoneType الحمد لله، واجهت مشكلة أغرب! عندي دالة بسيطة مفروض إنها تضيف عنصر لقائمة، وإذا ما أعطيتها قائمة تستخدم قائمة فاضية جديدة. المشكلة إنه الدالة بتتصرف كإنها بتتذكر القيم القديمة بين الاستدعاءات! يعني أول مرة بشغلها بتشتغل تمام، تاني مرة بتضيف العنصر الجديد للقائمة القديمة بدل ما تبدأ بقائمة فاضية. أنا مش فاهم إيه اللي بيحصل! هل بايثون فيها شبح؟ 🤔
هذا الكود اللي كتبته:
def اضافة_عنصر(عنصر, قائمتي=[]):قائمتي.append(عنصر)return قائمتيprint(اضافة_عنصر("تفاح")) # Output: ['تفاح'] (تمام)print(اضافة_عنصر("برتقال")) # Output: ['تفاح', 'برتقال'] (؟؟؟ ليش؟)
أرجو المساعدة!"
✅ الرد والحل العملي (لمن أراد حل مشكلته والمغادرة)
مرحبًا أحمد،
لا، بايثون ليس فيه أشباح، لكن فيه "فخ" كلاسيكي يقع فيه الجميع تقريبًا! هذا الفخ يُعرف باسم "الوسائط الافتراضية القابلة للتغيير" (Mutable Default Arguments). المشكلة ليست في الكود نفسه، بل في طريقة تعامل بايثون مع القيم الافتراضية للدوال.
السبب باختصار: القيمة الافتراضية [] (قائمة فارغة) لا يتم إنشاؤها من جديد عند كل استدعاء للدالة. بل يتم إنشاؤها مرة واحدة فقط عند تعريف الدالة لأول مرة، ويتم استخدام نفس الكائن (نفس القائمة) في جميع الاستدعاءات اللاحقة.
الحل العملي: لا تستخدم أبدًا كائنًا قابلًا للتغيير (مثل قائمة [] أو قاموس {} أو مجموعة ()set كقيمة افتراضية بشكل مباشر. استخدم None كقيمة افتراضية، ثم أنشئ الكائن الجديد داخل الدالة
بدلاً من كتابة هذا الكود الخاطئ:
def اضافة_عنصر(عنصر, قائمتي=[]): # <-- المصيبة هناقائمتي.append(عنصر)return قائمتي
استخدم هذا الكود الصحيح والآمن:
def اضافة_عنصر(عنصر, قائمتي=None):if قائمتي is None: # إذا لم يتم تمرير قائمة...قائمتي = [] # ...أنشئ قائمة جديدة تمامًاقائمتي.append(عنصر)return قائمتيprint(اضافة_عنصر("تفاح")) # ['تفاح']print(اضافة_عنصر("برتقال")) # ['برتقال'] <-- الحل السحري!
انتهى الأمر. بهذا التعديل البسيط ستحصل على السلوك المتوقع. هذه هي الطريقة المعتمدة في لغة بايثون لتفادي هذا المأزق
📚 الشرح الكامل للمشكلة وتفاصيلها (لمن يريد فهم الأسباب)
الآن بعد أن عرفت الحل، دعنا نفهم لماذا يحدث هذا السلوك الغريب. فهم السبب هو ما سيجعلك مبرمج بايثون محترفًا.
1. لماذا يحدث هذا؟ متى تُقيّم القيم الافتراضية؟
في بايثون، يتم تقييم (تنفيذ) القيم الافتراضية للوسائط (Arguments) مرة واحدة فقط، وذلك عند تعريف الدالة (Function Definition)، وليس عند استدعائها (Function Call).
بمعنى آخر، عندما يقرأ مفسّر بايثون الكود الخاص بك ويصل إلى سطر def اضافة_عنصر(... قائمتي=[]):، فإنه يقوم بتنفيذ [] مرة واحدة، وينشئ كائن قائمة واحد في الذاكرة. هذا الكائن الواحد هو ما سيتم استخدامه في كل مرة تستدعي فيها الدالة بدون تقديم قيمة للوسيط قائمتي.
2. مثال توضيحي للمشكلة خطوة بخطوة
لنأخذ الكود الخاطئ مرة أخرى:
def اضافة_عنصر(عنصر, قائمتي=[]):قائمتي.append(عنصر)return قائمتي
أثناء تعريف الدالة: بايثون ينشئ كائن قائمة فارغ في الذاكرة (لنسميه القائمة_الافتراضية_السرية).
الاستدعاء الأول اضافة_عنصر("تفاح"): بما أننا لم نمرر قيمة للوسيط قائمتي، تستخدم بايثون القائمة_الافتراضية_السرية. ثم نضيف "تفاح" إليها. تصبح محتوياتها ['تفاح'].
الاستدعاء الثاني اضافة_عنصر("برتقال"): مجددًا، لم نمرر قيمة للوسيط قائمتي، لذا تستمر بايثون في استخدام نفس القائمة_الافتراضية_السرية، والتي تحتوي بالفعل على "تفاح". نضيف "برتقال"، فتصبح ['تفاح', 'برتقال']. وهذا هو مصدر الدهشة!
3. كيف يعمل الحل (استخدام None)؟
الحل يعتمد على مبدأ بسيط: None هو كائن غير قابل للتغيير (Immutable). عند استخدام None كقيمة افتراضية، لن يتم تعديله أبدًا.
def اضافة_عنصر(عنصر, قائمتي=None): # 1if قائمتي is None: # 2قائمتي = [] # 3قائمتي.append(عنصر) # 4return قائمتي # 5
(1) القيمة الافتراضية هي None، وهي آمنة.
(2) عند كل استدعاء، نتحقق: هل الوسيط قائمتي ما زال None؟ (أي لم يتم تمرير قيمة له).
(3) إذا كان كذلك، نقوم بإنشاء قائمة فارغة جديدة تمامًا داخل الدالة. هذه القائمة تُنشأ في كل استدعاء، لذا فهي منفصلة تمامًا عن أي استدعاء سابق.
(4) نضيف العنصر إلى القائمة الجديدة.
(5) نعيد القائمة.
بهذه الطريقة، نضمن أن كل استدعاء للدالة بدون قائمة يحصل على قائمة جديدة خاصة به.
✨ نصائح إضافية من "بايثون العرب"
قاعدة ذهبية: لا تستخدم أبدًا [] أو {} أو ()set كقيمة افتراضية بشكل مباشر. استخدم دائمًا None وافحصه داخل الدالة.
متى يكون هذا مفيدًا؟ في حالات نادرة جدًا، قد ترغب في "استغلال" هذا السلوك لتخزين حالة بين استدعاءات الدالة (مثل عمل دالة للذاكرة المؤقتة cache). لكن في 99.9% من الحالات، يكون هذا السلوك غير مرغوب فيه وهو مصدر للأخطاء.
هذا الفخ موجود في كل مكان: ليس فقط في الكود الخاص بك. ستجده في مئات الآلاف من المشاريع مفتوحة المصدر على GitHub، وهو من أكثر الأسئلة تكرارًا على منصات مثل Stack Overflow.
أتمنى أن يكون هذا الشرح قد أزال الغموض، أخي أحمد. تذكر، المبرمج المحترف هو من يفهم ليس فقط كيف يكتب الكود، بل كيف يعمل الكود خلف الكواليس.
بالتوفيق دائمًا، وإلى لقاء في مشكلة وحل جديدة!
