مشكلة Mutable Default Arguments في بايثون تظهر عندما تستخدم كائنًا قابلًا للتغيير مثل [] أو {} أو set() كقيمة افتراضية لمعامل في دالة، ثم تعدّل هذا الكائن داخل الدالة. السبب أن بايثون تقيّم القيمة الافتراضية مرة واحدة عند تعريف الدالة، ثم تعيد استخدام الكائن نفسه في الاستدعاءات التالية التي لا تمرر قيمة بديلة.
الحل المعتاد هو استخدام None كقيمة افتراضية، ثم إنشاء List أو Dictionary أو Set جديدة داخل الدالة عند الحاجة. بهذه الطريقة يحصل كل استدعاء لا يمرر كائنًا على كائن جديد مستقل، ولا تنتقل تغييرات الاستدعاء السابق إلى الاستدعاء التالي.
{alertSuccess} الحل المختصر: بدلdef add_item(item, items=[])استخدمdef add_item(item, items=None)، ثم افحصif items is None:وأنشئ القائمة داخل الدالة.
{getToc} $title={محتوى المقال}
مثال يوضح مشكلة Mutable Default Arguments
افترض أنك تريد دالة تضيف عنصرًا إلى قائمة، وإذا لم يمرر المستخدم قائمة تبدأ الدالة بقائمة فارغة:
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("apple"))
print(add_item("orange"))
قد تتوقع:
['apple']
['orange']
لكن النتيجة الفعلية هي:
['apple']
['apple', 'orange']
السبب ليس أن الدالة «تتذكر» القيم بطريقة خاصة، بل أن الوسيط items يشير إلى نفس List الافتراضية في الاستدعاءين.
لماذا تحتفظ القائمة بالقيم بين الاستدعاءات؟
عند تنفيذ تعريف الدالة، تنشئ بايثون القيمة الافتراضية مرة واحدة. إذا كانت هذه القيمة List ثم عدّلتها باستخدام append()، فقد تغيّر نفس الكائن الذي سيُستخدم لاحقًا عند استدعاء الدالة بدون تمرير items.
يمكن إثبات أن النتيجتين تشيران إلى الكائن نفسه باستخدام is:
def add_item(item, items=[]):
items.append(item)
return items
first = add_item("apple")
second = add_item("orange")
print(first is second)
الناتج:
True
هذا أدق من وصف id() بأنه «عنوان الذاكرة». الدالة id() تعطي هوية فريدة للكائن أثناء حياته، لكن مواصفة بايثون لا تشترط أن تكون هذه القيمة عنوان الذاكرة. للتوسع راجع الفرق بين == و is في بايثون.
ما المقصود بـ Mutable Default Argument؟
المقصود هو استخدام كائن قابل للتغيير كقيمة افتراضية لمعامل في دالة. أمثلة شائعة:
def example(items=[]):
pass
def example(settings={}):
pass
def example(tags=set()):
pass
| نوع الكائن | Mutable؟ | مثال |
|---|---|---|
| List | نعم | [] |
| Dictionary | نعم | {} |
| Set | نعم | set() |
| String / int / float | لا | "guest"، 0 |
| Tuple | الـTuple نفسها غير قابلة للتعديل | (1, 2) |
المشكلة ليست في وجود قيمة افتراضية بحد ذاتها، بل في تعديل كائن افتراضي مشترك. إذا كنت تحتاج مراجعة أساس الدوال والمعاملات أولًا، اقرأ شرح Functions في بايثون للمبتدئين.
الحل الصحيح باستخدام None
استخدم None كقيمة Sentinel تعني «لم يمرر المستخدم قائمة»، ثم أنشئ List جديدة داخل جسم الدالة:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item("apple"))
print(add_item("orange"))
الناتج الصحيح:
['apple']
['orange']
كل استدعاء لا يمرر items يصل إلى الشرط وينشئ List جديدة. أما إذا مررت List بنفسك، فالدالة ستستخدم هذه القائمة التي مررتها وتعدلها باستخدام append().
{alertInfo}
مهم: استخدام None يمنع مشاركة القيمة الافتراضية بين الاستدعاءات، لكنه لا يمنع تعديل List يمررها المستخدم صراحة إلى الدالة.
لماذا نستخدم is None بدل == None؟
لأن None Singleton خاص في بايثون، والطريقة الموصى بها لمقارنته هي is None أو is not None:
if items is None:
items = []
ولا تستخدم if not items: بدلًا منها هنا، لأن List فارغة مررها المستخدم عمدًا ستُعامل أيضًا كقيمة False. إذا أردت شرحًا أوسع لقيمة None نفسها، راجع شرح None في بايثون.
هل المشكلة تحدث مع List فقط؟
لا. نفس الفكرة تنطبق على أي قيمة افتراضية قابلة للتعديل. أكثر الأمثلة شيوعًا هي List وDictionary وSet.
| تجنب | استخدم عادة |
|---|---|
items=[] |
items=None ثم items = [] |
settings={} |
settings=None ثم settings = {} |
tags=set() |
tags=None ثم tags = set() |
مثال مختصر مع Dictionary
def add_setting(key, value, settings=None):
if settings is None:
settings = {}
settings[key] = value
return settings
لا نحتاج إلى تكرار مثال كامل مستقل لكل نوع؛ القاعدة نفسها هي إنشاء الكائن القابل للتغيير داخل الدالة عندما لا يمرر المستدعي قيمة.
هل Mutable Default Argument خطأ دائمًا؟
لا. هذا السلوك جزء مقصود من لغة بايثون وليس Bug في المفسر. يمكن استخدام قيمة افتراضية مشتركة عمدًا للاحتفاظ بحالة بين الاستدعاءات، وتذكر وثائق بايثون مثالًا على الاستفادة من ذلك في Caching.
لكن عندما تكون نية الدالة أن يبدأ كل استدعاء بقائمة أو قاموس جديد، فإن Mutable Default يؤدي إلى حالة مشتركة مخفية وسلوك مربك. لذلك القاعدة العملية للمبتدئ هي استخدام None عندما تريد كائنًا جديدًا لكل استدعاء.
{alertWarning}
لا تحفظ قاعدة «Mutable Defaults ممنوعة» دون فهم السبب. اسأل: هل أريد نفس الكائن أن يُعاد استخدامه عمدًا، أم أريد كائنًا جديدًا لكل استدعاء؟ في الحالة الثانية استخدم None.
أخطاء شائعة عند إصلاح المشكلة
- استخدام
if not items: هذا يخلط بينNoneوList فارغة مررت عمدًا. - استخدام
== None: الأفضل معNoneهوis None. - الاعتقاد أن كل قيمة افتراضية تُنشأ مع كل استدعاء: تعبير القيمة الافتراضية يُقيّم عند تعريف الدالة.
- الاعتقاد أن الحل يمنع تعديل القائمة الممررة: إذا مررت List صراحة، فإن
append()سيعدلها ما لم تنسخها داخل الدالة. - حفظ النمط دون فهم Functions: فهم Parameters وDefault Arguments يجعل هذه المشكلة أسهل بكثير.
اختبار سريع لنفسك
ما نتيجة الكود التالي في الاستدعاء الثاني؟
def add_task(task, tasks=[]):
tasks.append(task)
return tasks
print(add_task("Learn functions"))
print(add_task("Solve exercise"))
الإجابة: الاستدعاء الثاني سيحتوي المهمتين لأنهما أضيفتا إلى نفس List الافتراضية. أصلح الدالة باستخدام tasks=None وإنشاء [] داخلها.
أسئلة شائعة
ما معنى Mutable Default Arguments في بايثون؟
تعني استخدام كائن قابل للتعديل كقيمة افتراضية لمعامل في دالة. إذا عدلت هذا الكائن، فقد ترى التغيير في استدعاءات لاحقة لأن القيمة الافتراضية نفسها يعاد استخدامها.
لماذا تتذكر List القيم من الاستدعاء السابق؟
لأن List الافتراضية أُنشئت عندما تم تعريف الدالة، وليست List جديدة تُنشأ تلقائيًا عند كل استدعاء.
ما الحل الصحيح عندما أريد List جديدة كل مرة؟
استخدم None كقيمة افتراضية، ثم اكتب if items is None: items = [] داخل الدالة.
هل المشكلة تحدث مع dict وset أيضًا؟
نعم. إذا استخدمت Dictionary أو Set قابلة للتعديل كقيمة افتراضية ثم عدلتها، يمكن أن تُشارك حالتها بين الاستدعاءات بالطريقة نفسها.
هل القيم الافتراضية غير القابلة للتعديل آمنة؟
لا يحدث معها هذا النوع من تراكم التغييرات لأن الكائن نفسه لا يُعدّل في مكانه. لكن تظل القاعدة العامة أن القيمة الافتراضية تُقيّم مرة واحدة عند تعريف الدالة.
مصادر رسمية
- Python Tutorial — Default Argument Values
- Python FAQ — Why are default values shared between objects?
- PEP 8 — المقارنة مع None باستخدام is وis not
الخلاصة
مشكلة Mutable Default Arguments تحدث لأن القيم الافتراضية للدالة تُقيّم مرة واحدة عند تعريفها. إذا كانت القيمة الافتراضية List أو Dictionary أو Set ثم عدّلتها، فسيُعاد استخدام الكائن المعدّل في الاستدعاءات التالية التي لا تمرر قيمة بديلة.
عندما تريد كائنًا جديدًا لكل استدعاء، استخدم None كقيمة افتراضية ثم أنشئ الكائن داخل الدالة. واستخدم is None عند الفحص بدل الاعتماد على Truthiness.
{alertSuccess}
القاعدة العملية: إذا كان المطلوب أن يبدأ كل استدعاء بحاوية جديدة، لا تضع الحاوية نفسها في توقيع الدالة؛ استخدم None وأنشئها داخل جسم الدالة.
إذا كنت تريد متابعة أخطاء مشابهة، راجع سلسلة مشكلة وحل في بايثون.



