cb
codebamin
الفصل 6 · القوائم (Lists)

مخاطر تعديل القائمة أثناء التكرار عليها

انظر إلى هذا البرنامج. يمر على قائمة من الأعداد ويحاول حذف الأعداد الأصغر من 10. أو على الأقل هذا ما يحاول فعله. شغّله وراقب حالة البرنامج خطوة بخطوة:

__copyable__ __program_indented__

تذكر أن numbers.pop(i) تحذف من numbers العنصر الموجود عند الفهرس i.

أثناء التنفيذ ستلاحظ بوضوح أن البرنامج يتجاوز العددين 7 و3 ولا يحذفهما، ثم يفشل في النهاية عندما يحاول الوصول إلى فهرس أكبر من حدود القائمة. هل ترى لماذا؟

المتغير i يمر بالقيم المعتادة 0 و1 و2 وهكذا كما يفترض، لكن القائمة نفسها تتغير أثناء ذلك، ولذلك لا تعود هذه الأرقام تشير إلى المواضع التي نتوقعها. في الدورة الأولى مثلًا تكون i مساوية 0 ويكون number هو 10، فيُحذف. عندها تتحرك بقية العناصر موضعًا واحدًا إلى اليسار، فيصبح 7 في الموضع 0. لكن في الدورة التالية تصبح i مساوية 1، فيكون numbers[i] هو 8؛ وهكذا تم تجاوز 7.

قد نحاول كتابة البرنامج باستخدام remove بدل pop حتى لا نحتاج إلى الفهارس. بل يبدو الكود أجمل بهذه الطريقة:

__program_indented__

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

القاعدة المهمة هنا هي: لا تعدّل الشيء نفسه أثناء التكرار عليه. افصل التعديل عن المرور بالحَلْقة.

والخبر الجيد أن هناك عدة حلول. يمكنك مثلًا التكرار على نسخة من القائمة:

for number in numbers.copy():

أصبحت القائمة التي تعدلها والقائمة التي تتكرر عليها كائنين منفصلين، حتى لو بدأتا بالمحتوى نفسه.

وبطريقة مشابهة يمكنك التكرار على القائمة الأصلية وتعديل نسخة منها:

__program_indented__

أو يمكنك بناء قائمة جديدة من الصفر. وقد فعلنا شيئًا مشابهًا بالفعل في تمرين سابق:

__program_indented__

كرر القاعدة الأساسية: لا تعدّل الشيء نفسه أثناء المرور عليه تكراريًا. لديك ثلاثة أساليب آمنة شائعة:

  • عدّل نسخة مستقلة.
  • مرّ تكراريًا على نسخة بينما تعدّل الأصل.
  • لا تعدّل الأصل أصلًا؛ ابنِ نسخة جديدة بالنتيجة المطلوبة.
solution.py
1
2
3
4
5
6
7
8
9
10
الطرفية (shell)
>>>