حل مشكلة Mutable Default Arguments في بايثون | مشكلة وحل 2

حل مشكلة Mutable Default Arguments في Python عند استخدام قائمة كقيمة افتراضية

مشكلة 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']
شرح فكرة Mutable Default Arguments في Python ولماذا تتذكر القائمة القيم القديمة

السبب ليس أن الدالة «تتذكر» القيم بطريقة خاصة، بل أن الوسيط items يشير إلى نفس List الافتراضية في الاستدعاءين.

لماذا تحتفظ القائمة بالقيم بين الاستدعاءات؟

عند تنفيذ تعريف الدالة، تنشئ بايثون القيمة الافتراضية مرة واحدة. إذا كانت هذه القيمة List ثم عدّلتها باستخدام append()، فقد تغيّر نفس الكائن الذي سيُستخدم لاحقًا عند استدعاء الدالة بدون تمرير items.

مثال يوضح خطأ استخدام list كقيمة افتراضية في دالة Python

يمكن إثبات أن النتيجتين تشيران إلى الكائن نفسه باستخدام 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']
حل مشكلة Mutable Default Arguments باستخدام None داخل دوال Python

كل استدعاء لا يمرر 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 يجعل هذه المشكلة أسهل بكثير.
أفضل ممارسات تجنب Mutable Default Arguments في Python مع list و dict و set

اختبار سريع لنفسك

ما نتيجة الكود التالي في الاستدعاء الثاني؟

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 قابلة للتعديل كقيمة افتراضية ثم عدلتها، يمكن أن تُشارك حالتها بين الاستدعاءات بالطريقة نفسها.

هل القيم الافتراضية غير القابلة للتعديل آمنة؟

لا يحدث معها هذا النوع من تراكم التغييرات لأن الكائن نفسه لا يُعدّل في مكانه. لكن تظل القاعدة العامة أن القيمة الافتراضية تُقيّم مرة واحدة عند تعريف الدالة.

مصادر رسمية

الخلاصة

مشكلة Mutable Default Arguments تحدث لأن القيم الافتراضية للدالة تُقيّم مرة واحدة عند تعريفها. إذا كانت القيمة الافتراضية List أو Dictionary أو Set ثم عدّلتها، فسيُعاد استخدام الكائن المعدّل في الاستدعاءات التالية التي لا تمرر قيمة بديلة.

عندما تريد كائنًا جديدًا لكل استدعاء، استخدم None كقيمة افتراضية ثم أنشئ الكائن داخل الدالة. واستخدم is None عند الفحص بدل الاعتماد على Truthiness.

{alertSuccess} القاعدة العملية: إذا كان المطلوب أن يبدأ كل استدعاء بحاوية جديدة، لا تضع الحاوية نفسها في توقيع الدالة؛ استخدم None وأنشئها داخل جسم الدالة.

إذا كنت تريد متابعة أخطاء مشابهة، راجع سلسلة مشكلة وحل في بايثون.

إرسال تعليق

أحدث أقدم