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

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

أحيانًا تكتب دالة بسيطة في Python، وتتوقع أن تبدأ من الصفر في كل مرة تستدعيها، ثم تكتشف أنها تحتفظ بنتائج الاستدعاء السابق! هذا السلوك يربك كثيرًا من المبتدئين، خصوصًا عند استخدام قائمة [] أو قاموس {} كقيمة افتراضية داخل الدالة.

في هذا المقال من سلسلة مشكلة وحل في بايثون سنشرح مشكلة Mutable Default Arguments بطريقة عملية ومفصلة: لماذا تحدث؟ ما علاقة القوائم والقواميس بها؟ لماذا تتذكر الدالة القيم القديمة؟ وما الحل الصحيح باستخدام None؟

{getToc} $title={محتوى المقال}

الفكرة ببساطة: لا تستخدم قائمة [] أو قاموس {} أو مجموعة set() كقيمة افتراضية مباشرة في دوال Python؛ لأن هذه القيم تُنشأ مرة واحدة فقط عند تعريف الدالة، وليس عند كل استدعاء. {alertInfo}

سيناريو المشكلة

تخيل أن أحمد، أحد متابعي بايثون العرب، كتب دالة بسيطة لإضافة عنصر إلى قائمة. الفكرة تبدو سهلة: إذا مررنا قائمة للدالة تضيف العنصر إليها، وإذا لم نمرر قائمة تبدأ بقائمة فارغة.

الكود الذي كتبه كان بهذا الشكل:

def اضافة_عنصر(عنصر, قائمتي=[]):
    قائمتي.append(عنصر)
    return قائمتي


print(اضافة_عنصر("تفاح"))
print(اضافة_عنصر("برتقال"))

المتوقع من وجهة نظر المبتدئ أن تكون النتيجة:

['تفاح']
['برتقال']

لكن النتيجة الفعلية تكون غالبًا:

['تفاح']
['تفاح', 'برتقال']

وهنا يبدأ السؤال: لماذا تذكرت الدالة عنصر "تفاح" من الاستدعاء السابق؟ هل القائمة لم تعد فارغة؟ وهل بايثون يحتفظ بشيء خلف الكواليس؟


شرح فكرة Mutable Default Arguments في Python ولماذا تتذكر القائمة القيم القديمة

الحل السريع

الحل الصحيح هو ألا نضع [] كقيمة افتراضية مباشرة. بدلًا من ذلك نستخدم 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 ينشئ القائمة الفارغة [] مرة واحدة فقط لحظة تعريف الدالة. وبعد ذلك يتم استخدام نفس القائمة في كل استدعاء لا يمرر قيمة للوسيط قائمتي.

بمعنى آخر، القائمة الافتراضية لا يتم إنشاؤها من جديد في كل مرة. إنها قائمة واحدة مشتركة بين الاستدعاءات.


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

شرح المشكلة خطوة بخطوة

لنشرح ما يحدث في الكود الخاطئ:

def اضافة_عنصر(عنصر, قائمتي=[]):
    قائمتي.append(عنصر)
    return قائمتي
  1. عند تعريف الدالة، ينشئ Python قائمة فارغة واحدة فقط.
  2. في الاستدعاء الأول، لا نمرر قائمة، فيستخدم Python القائمة الافتراضية.
  3. تتم إضافة "تفاح" إلى نفس القائمة الافتراضية.
  4. في الاستدعاء الثاني، لا نمرر قائمة أيضًا، فيستخدم Python نفس القائمة السابقة.
  5. تتم إضافة "برتقال" إلى القائمة نفسها، لذلك تظهر القيمتان معًا.

هذا يفسر لماذا النتيجة الثانية تحتوي على "تفاح" و "برتقال" معًا.

إثبات المشكلة باستخدام 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 قيمة آمنة لأنها غير قابلة للتغيير.
  • كل استدعاء بدون قائمة ينشئ قائمة جديدة داخل الدالة.
  • إذا مرر المستخدم قائمة بنفسه، تستخدم الدالة القائمة التي مررها.

حل مشكلة Mutable Default Arguments باستخدام None داخل دوال Python

لماذا نستخدم 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 في Python مع list و dict و set

أفضل ممارسات لتجنب Mutable Default Arguments

  • لا تستخدم [] كقيمة افتراضية داخل الدوال.
  • لا تستخدم {} كقيمة افتراضية داخل الدوال.
  • لا تستخدم set() كقيمة افتراضية داخل الدوال.
  • استخدم None كقيمة افتراضية آمنة.
  • افحص القيمة باستخدام is None.
  • أنشئ القائمة أو القاموس داخل الدالة.
  • لا تعتمد على أن القيمة الافتراضية ستُنشأ من جديد عند كل استدعاء.
  • استخدم أسماء واضحة للوسائط حتى تفهم وظيفة الدالة بسهولة.

تدريب عملي

حاول إصلاح الدالة التالية بنفسك قبل النظر إلى الحل:

def اضافة_مهمة(مهمة, مهام=[]):
    مهام.append(مهمة)
    return مهام

المطلوب:

  1. استبدل القيمة الافتراضية [] بـ None.
  2. افحص هل المتغير مهام يساوي None.
  3. إذا كان كذلك، أنشئ قائمة فارغة جديدة.
  4. أضف المهمة إلى القائمة.
  5. اختبر الدالة باستدعاءين منفصلين.

حل التدريب

def اضافة_مهمة(مهمة, مهام=None):
    if مهام is None:
        مهام = []

    مهام.append(مهمة)
    return مهام


print(اضافة_مهمة("تعلم الدوال"))
print(اضافة_مهمة("حل تمرين"))

الناتج:

['تعلم الدوال']
['حل تمرين']

روابط داخلية مفيدة من بايثون العرب

مصادر خارجية مفيدة للتوسع

الخلاصة

مشكلة 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 هي الطريقة المفضلة والأوضح.

إرسال تعليق

أحدث أقدم