المستوى: مبتدئ — لو لسه بتتعلم بايثون وبتكتب دوال، المقال ده هيوفّرلك ساعات من البحث عن باج غريب مش بيظهر إلا بعد تشغيل تاني.
لو دالة بتاعتك المفروض ترجّع قائمة فاضية في كل مرة، وطلعت مليانة بقيم من نداءات قديمة، ده مش عطل ولا اختراق. ده أشهر فخ في بايثون: الوسيط الافتراضي القابل للتغيير (Mutable Default Argument).
فخ الوسيط الافتراضي في بايثون: القائمة اللي مش بتنسى
المشكلة باختصار
بتكتب دالة بتاخد قائمة اختيارية، وبتحط لها قيمة افتراضية [] عشان لو الشخص ما بعتش حاجة تبدأ من قائمة فاضية. المنطق سليم في دماغك. لكن اللي بيحصل فعلاً إن القائمة دي بتتعمل مرة واحدة بس، ونفس القائمة بتترجع في كل نداء. النتيجة: القيم بتتراكم بدل ما تبدأ من الصفر.
خليها بمثال قبل الكلام العلمي
تخيّل إنك اشتغلت في مكتب، وأول يوم المدير قالك: "خُد الكراسة اللي على المكتب واكتب فيها المهام بتاعتك." أنت افترضت إنك هتلاقي كراسة جديدة فاضية كل يوم. لكن في الحقيقة، فيه كراسة واحدة بس على المكتب، وكل موظف بيكتب فيها فوق اللي قبله. تيجي بعد أسبوع تلاقي الكراسة مليانة كلام مش بتاعك.
ده بالظبط اللي بيعمله box=[]. الكراسة (القائمة) اتحطّت على المكتب مرة واحدة يوم ما الدالة اتعرّفت، ومن يومها الكل بيكتب في نفس الكراسة.
ليه بيحصل ده علميًا؟
ركز في النقطة دي، لأنها سبب المشكلة كلها: القيمة الافتراضية للوسيط بتتحسب مرة واحدة وقت تعريف الدالة (def)، مش في كل نداء. يعني [] اللي كتبتها بتتنفّذ ساعة ما بايثون بيقرأ سطر الـ def، وبتتخزّن جوّه الدالة نفسها في خاصية اسمها __defaults__.
القوائم في بايثون قابلة للتغيير (mutable)، فلما تعمل append أنت بتعدّل نفس الكائن المخزّن، مش بتعمل واحد جديد. عشان كده التعديلات بتفضل موجودة للنداء اللي بعده.
الكود اللي بيوريك المشكلة
# الطريقة الغلط
def add_item(item, box=[]):
box.append(item)
return box
print(add_item("a")) # ['a'] متوقّع
print(add_item("b")) # ['a', 'b'] مش متوقّع!
print(add_item("c")) # ['a', 'b', 'c'] الكارثة
عايز دليل ملموس إن دي نفس القائمة مش قائمة جديدة؟ اطبع عنوانها في الذاكرة بـ id():
print(id(add_item("x"))) # مثلاً: 140244665929344
print(id(add_item("y"))) # نفس الرقم بالظبط: 140244665929344