أحيانًا تكتب دالة بسيطة في Python، وتتوقع أن تبدأ من الصفر في كل مرة تستدعيها، ثم تكتشف أنها تحتفظ بنتائج الاستدعاء السابق! هذا السلوك يربك كثيرًا من المبتدئين، خصوصًا عند استخدام قائمة [] أو قاموس {} كقيمة افتراضية داخل الدالة.
في هذا المقال من سلسلة مشكلة وحل في بايثون سنشرح مشكلة Mutable Default Arguments بطريقة عملية ومفصلة: لماذا تحدث؟ ما علاقة القوائم والقواميس بها؟ لماذا تتذكر الدالة القيم القديمة؟ وما الحل الصحيح باستخدام None؟
{getToc} $title={محتوى المقال}
الفكرة ببساطة: لا تستخدم قائمة[]أو قاموس{}أو مجموعةset()كقيمة افتراضية مباشرة في دوال Python؛ لأن هذه القيم تُنشأ مرة واحدة فقط عند تعريف الدالة، وليس عند كل استدعاء. {alertInfo}
سيناريو المشكلة
تخيل أن أحمد، أحد متابعي بايثون العرب، كتب دالة بسيطة لإضافة عنصر إلى قائمة. الفكرة تبدو سهلة: إذا مررنا قائمة للدالة تضيف العنصر إليها، وإذا لم نمرر قائمة تبدأ بقائمة فارغة.
الكود الذي كتبه كان بهذا الشكل:
def اضافة_عنصر(عنصر, قائمتي=[]):
قائمتي.append(عنصر)
return قائمتي
print(اضافة_عنصر("تفاح"))
print(اضافة_عنصر("برتقال"))
المتوقع من وجهة نظر المبتدئ أن تكون النتيجة:
['تفاح']
['برتقال']
لكن النتيجة الفعلية تكون غالبًا:
['تفاح']
['تفاح', 'برتقال']
وهنا يبدأ السؤال: لماذا تذكرت الدالة عنصر "تفاح" من الاستدعاء السابق؟ هل القائمة لم تعد فارغة؟ وهل بايثون يحتفظ بشيء خلف الكواليس؟
الحل السريع
الحل الصحيح هو ألا نضع [] كقيمة افتراضية مباشرة. بدلًا من ذلك نستخدم None، ثم ننشئ القائمة داخل الدالة.
def اضافة_عنصر(عنصر, قائمتي=None):
if قائمتي is None:
قائمتي = []
قائمتي.append(عنصر)
return قائمتي
print(اضافة_عنصر("تفاح"))
print(اضافة_عنصر("برتقال"))
الناتج الآن سيكون كما نتوقع:
['تفاح']
['برتقال']
بهذا التعديل البسيط، كل استدعاء للدالة بدون قائمة يحصل على قائمة جديدة خاصة به، بدل استخدام نفس القائمة القديمة.
الخلاصة السريعة: إذا احتجت إلى قائمة افتراضية داخل دالة، استخدم None كقيمة افتراضية، ثم أنشئ القائمة داخل الدالة. {alertSuccess}
ما معنى Mutable Default Arguments؟
مصطلح Mutable Default Arguments يعني: الوسائط الافتراضية القابلة للتغيير. لنقسم المصطلح:
- Arguments: القيم التي نمررها للدالة.
- Default: قيمة افتراضية تستخدمها الدالة إذا لم نمرر قيمة.
- Mutable: كائن قابل للتغيير بعد إنشائه، مثل القائمة والقاموس والمجموعة.
إذًا المشكلة تظهر عندما نستخدم كائنًا قابلًا للتغيير كقيمة افتراضية للدالة، مثل:
def example(items=[]):
pass
def example(settings={}):
pass
def example(values=set()):
pass
هذه الصيغ قد تبدو طبيعية، لكنها قد تؤدي إلى نتائج غير متوقعة إذا تم تعديل القيمة الافتراضية داخل الدالة.
لماذا تحدث المشكلة؟
السبب الأساسي أن Python يقيّم القيم الافتراضية للدالة مرة واحدة فقط عند تعريف الدالة، وليس في كل مرة تستدعي فيها الدالة.
عندما تكتب:
def اضافة_عنصر(عنصر, قائمتي=[]):
قائمتي.append(عنصر)
return قائمتي
فإن Python ينشئ القائمة الفارغة [] مرة واحدة فقط لحظة تعريف الدالة. وبعد ذلك يتم استخدام نفس القائمة في كل استدعاء لا يمرر قيمة للوسيط قائمتي.
بمعنى آخر، القائمة الافتراضية لا يتم إنشاؤها من جديد في كل مرة. إنها قائمة واحدة مشتركة بين الاستدعاءات.
شرح المشكلة خطوة بخطوة
لنشرح ما يحدث في الكود الخاطئ:
def اضافة_عنصر(عنصر, قائمتي=[]):
قائمتي.append(عنصر)
return قائمتي
- عند تعريف الدالة، ينشئ Python قائمة فارغة واحدة فقط.
- في الاستدعاء الأول، لا نمرر قائمة، فيستخدم Python القائمة الافتراضية.
- تتم إضافة
"تفاح"إلى نفس القائمة الافتراضية. - في الاستدعاء الثاني، لا نمرر قائمة أيضًا، فيستخدم Python نفس القائمة السابقة.
- تتم إضافة
"برتقال"إلى القائمة نفسها، لذلك تظهر القيمتان معًا.
هذا يفسر لماذا النتيجة الثانية تحتوي على "تفاح" و "برتقال" معًا.
إثبات المشكلة باستخدام id
يمكنك التأكد أن Python يستخدم نفس القائمة من خلال الدالة id، وهي تعرض معرف الكائن في الذاكرة.
def اضافة_عنصر(عنصر, قائمتي=[]):
قائمتي.append(عنصر)
print("ID:", id(قائمتي))
return قائمتي
print(اضافة_عنصر("تفاح"))
print(اضافة_عنصر("برتقال"))
ستلاحظ غالبًا أن قيمة ID متشابهة في الاستدعاءين، وهذا يعني أننا نتعامل مع نفس كائن القائمة.
ما الفرق بين Mutable و Immutable؟
لفهم المشكلة بعمق، يجب أن تعرف الفرق بين الكائنات القابلة للتغيير والكائنات غير القابلة للتغيير.
| النوع | المعنى | أمثلة |
|---|---|---|
| Mutable | كائن يمكن تغييره بعد إنشائه | list, dict, set |
| Immutable | كائن لا يتغير بعد إنشائه | str, int, float, tuple, None |
المشكلة تظهر غالبًا مع الأنواع القابلة للتغيير مثل القوائم والقواميس؛ لأنها يمكن أن تتغير داخل الدالة وتبقى محتفظة بالتغيير في الاستدعاءات التالية.
لماذا لا تظهر المشكلة مع الأرقام والنصوص؟
إذا كتبت دالة تستخدم رقمًا أو نصًا كقيمة افتراضية، غالبًا لن تواجه نفس المشكلة؛ لأن الأرقام والنصوص غير قابلة للتغيير.
def رحب(اسم="زائر"):
return "مرحبًا " + اسم
print(رحب())
print(رحب("علي"))
هنا لا نعدل النص الافتراضي نفسه، لذلك لا تظهر المشكلة. الخطر الأكبر عندما تكون القيمة الافتراضية كائنًا يمكن تعديله مثل [] أو {}.
الحل الصحيح باستخدام None
الحل المعتمد في Python هو استخدام None كقيمة افتراضية، ثم إنشاء القائمة داخل الدالة إذا كانت القيمة ما زالت None.
def اضافة_عنصر(عنصر, قائمتي=None):
if قائمتي is None:
قائمتي = []
قائمتي.append(عنصر)
return قائمتي
في هذه الحالة:
Noneقيمة آمنة لأنها غير قابلة للتغيير.- كل استدعاء بدون قائمة ينشئ قائمة جديدة داخل الدالة.
- إذا مرر المستخدم قائمة بنفسه، تستخدم الدالة القائمة التي مررها.
لماذا نستخدم is None وليس == None؟
قد تكتب:
if قائمتي == None:
قائمتي = []
هذا قد يعمل في حالات كثيرة، لكن الأفضل في Python هو استخدام:
if قائمتي is None:
قائمتي = []
لأن None كائن خاص وفريد في Python، والمقارنة معه تكون عادة باستخدام is. هذا الأسلوب أوضح وأكثر توافقًا مع أسلوب كتابة Python الجيد.
مثال مع قاموس dict
المشكلة لا تحدث مع القوائم فقط. يمكن أن تحدث أيضًا مع القواميس.
هذا مثال خاطئ:
def اضافة_اعداد(مفتاح, قيمة, اعدادات={}):
اعدادات[مفتاح] = قيمة
return اعدادات
print(اضافة_اعداد("theme", "dark"))
print(اضافة_اعداد("language", "Arabic"))
الناتج قد يكون:
{'theme': 'dark'}
{'theme': 'dark', 'language': 'Arabic'}
الحل الصحيح:
def اضافة_اعداد(مفتاح, قيمة, اعدادات=None):
if اعدادات is None:
اعدادات = {}
اعدادات[مفتاح] = قيمة
return اعدادات
مثال مع set
نفس الفكرة تنطبق على المجموعات set.
تجنب هذا:
def اضافة_وسم(وسم, وسوم=set()):
وسوم.add(وسم)
return وسوم
واستخدم هذا:
def اضافة_وسم(وسم, وسوم=None):
if وسوم is None:
وسوم = set()
وسوم.add(وسم)
return وسوم
جدول مقارنة بين الخطأ والحل
| الحالة | الطريقة الخاطئة | الطريقة الصحيحة |
|---|---|---|
| قائمة | items=[] |
items=None ثم items = [] |
| قاموس | settings={} |
settings=None ثم settings = {} |
| مجموعة | values=set() |
values=None ثم values = set() |
متى يكون هذا السلوك مقصودًا؟
في حالات نادرة ومتقدمة، قد يستخدم بعض المبرمجين هذا السلوك عمدًا لتخزين حالة بين استدعاءات الدالة، مثل بناء ذاكرة مؤقتة بسيطة cache. لكن هذا ليس مناسبًا للمبتدئين، وغالبًا يجعل الكود مربكًا لمن يقرأه.
لذلك القاعدة العملية لك كمبتدئ أو متوسط في Python:
قاعدة ذهبية: في 99% من الحالات، لا تستخدم كائنًا قابلًا للتغيير كقيمة افتراضية. استخدم None وأنشئ الكائن داخل الدالة. {alertWarning}
أخطاء شائعة مرتبطة بهذه المشكلة
1. استخدام [] كقيمة افتراضية
هذا هو الخطأ الأشهر:
def add_item(item, items=[]):
items.append(item)
return items
الصحيح:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
2. استخدام {} كقيمة افتراضية
القواميس أيضًا قابلة للتغيير، لذلك تجنب:
def save_user(name, data={}):
data["name"] = name
return data
3. الاعتقاد أن الدالة تبدأ من الصفر دائمًا
الدالة لا تنشئ القيمة الافتراضية من جديد دائمًا. القيم الافتراضية يتم تقييمها عند تعريف الدالة، وهذه نقطة مهمة جدًا في فهم Python.
4. استخدام if not بدل is None في حالات غير مناسبة
قد يكتب البعض:
if not items:
items = []
هذا قد يسبب سلوكًا غير مرغوب إذا مرر المستخدم قائمة فارغة عمدًا. الأفضل عند فحص القيمة الافتراضية أن تستخدم:
if items is None:
items = []
أفضل ممارسات لتجنب Mutable Default Arguments
- لا تستخدم
[]كقيمة افتراضية داخل الدوال. - لا تستخدم
{}كقيمة افتراضية داخل الدوال. - لا تستخدم
set()كقيمة افتراضية داخل الدوال. - استخدم
Noneكقيمة افتراضية آمنة. - افحص القيمة باستخدام
is None. - أنشئ القائمة أو القاموس داخل الدالة.
- لا تعتمد على أن القيمة الافتراضية ستُنشأ من جديد عند كل استدعاء.
- استخدم أسماء واضحة للوسائط حتى تفهم وظيفة الدالة بسهولة.
تدريب عملي
حاول إصلاح الدالة التالية بنفسك قبل النظر إلى الحل:
def اضافة_مهمة(مهمة, مهام=[]):
مهام.append(مهمة)
return مهام
المطلوب:
- استبدل القيمة الافتراضية
[]بـNone. - افحص هل المتغير
مهاميساويNone. - إذا كان كذلك، أنشئ قائمة فارغة جديدة.
- أضف المهمة إلى القائمة.
- اختبر الدالة باستدعاءين منفصلين.
حل التدريب
def اضافة_مهمة(مهمة, مهام=None):
if مهام is None:
مهام = []
مهام.append(مهمة)
return مهام
print(اضافة_مهمة("تعلم الدوال"))
print(اضافة_مهمة("حل تمرين"))
الناتج:
['تعلم الدوال']
['حل تمرين']
روابط داخلية مفيدة من بايثون العرب
- سلسلة مشكلة وحل في بايثون
- كورس أساسيات بايثون للمبتدئين
- أساسيات بايثون: شرح الدوال Functions
- شرح return في Python للمبتدئين
- شرح None في Python للمبتدئين
مصادر خارجية مفيدة للتوسع
- توثيق Python الرسمي حول القيم الافتراضية للوسائط
- دليل The Hitchhiker’s Guide to Python حول Mutable Default Arguments
الخلاصة
مشكلة Mutable Default Arguments من أشهر الفخاخ في Python. تظهر عندما تستخدم كائنًا قابلًا للتغيير مثل قائمة أو قاموس أو مجموعة كقيمة افتراضية داخل دالة. السبب أن القيم الافتراضية تُنشأ مرة واحدة فقط عند تعريف الدالة، وليس عند كل استدعاء.
الحل بسيط وواضح: استخدم None كقيمة افتراضية، ثم أنشئ القائمة أو القاموس داخل الدالة عند الحاجة. بهذه الطريقة تحصل كل عملية استدعاء على كائن جديد ومستقل، وتتجنب أن تتذكر الدالة القيم القديمة.
الخلاصة العملية: لا تكتبitems=[]أوdata={}في معاملات الدالة الافتراضية. اكتبitems=Noneأوdata=Noneثم أنشئ الكائن داخل الدالة. {alertSuccess}
أسئلة شائعة مع إجاباتها
ما معنى Mutable Default Arguments في Python؟
تعني استخدام كائن قابل للتغيير مثل list أو dict أو set كقيمة افتراضية في دالة، مما قد يجعل الدالة تحتفظ بالتغييرات بين الاستدعاءات.
لماذا تتذكر القائمة القيم القديمة داخل الدالة؟
لأن القائمة الافتراضية يتم إنشاؤها مرة واحدة فقط عند تعريف الدالة، ثم يعاد استخدام نفس القائمة في الاستدعاءات التالية التي لا تمرر قيمة بديلة.
ما الحل الصحيح لهذه المشكلة؟
استخدم None كقيمة افتراضية، ثم افحصها داخل الدالة باستخدام is None، وبعدها أنشئ قائمة أو قاموسًا جديدًا.
هل المشكلة تحدث مع list فقط؟
لا. تحدث مع أي كائن قابل للتغيير مثل list و dict و set.
هل يمكن استخدام string أو int كقيمة افتراضية؟
نعم غالبًا بدون هذه المشكلة؛ لأن النصوص والأرقام غير قابلة للتغيير، لكن يجب دائمًا فهم طبيعة القيمة التي تستخدمها.
لماذا نستخدم is None بدل == None؟
لأن None كائن خاص وفريد في Python، والمقارنة معه باستخدام is هي الطريقة المفضلة والأوضح.



