الفصل 12

القوائم — قيم متعددة تحت اسم واحد

الاحتفاظ بأي عدد تريده من القيم تحت اسم واحد، والوصول إليها عبر الفهارس والشرائح، ودوال تعديل القوائم — مع تجنب فخ ربط اسمين بنفس القائمة.

30 دقيقةPython 3.12
  1. 1المشكلة
  2. 2الفهم
  3. 3أمثلة محلولة
  4. 4التوقع
  5. 5التطبيق
  6. 6التحدي

المشكلة التي نقوم بحلها

يحتاج متجر ما إلى تخزين أسعار ثلاثة منتجات. وفقاً لما تعلمناه حتى الآن، لا توجد سوى طريقة واحدة للقيام بذلك:

python
price_1 = 15
price_2 = 60
price_3 = 850

print(price_1 + price_2 + price_3)
text
925

والآن يطرح السؤال نفسه: ماذا لو كان المتجر يبيع أربعين منتجاً؟ هذا يعني أربعين متغيراً! وسطر حسابات طويل يحتوي على أربعين طرفاً مجموعاً. وعندما يبدأ المتجر في بيع سلعة جديدة، سيكون عليك الرجوع إلى الكود وابتكار اسم متغير جديد.

المشكلة لا تكمن فقط في كثرة الكتابة، بل في البنية الهيكلية. فبالنسبة لبايثون، تُعتبر الأسماء price_1 و price_2 و price_3 ثلاثة متغيرات غير مترابطة تماماً — تماماً مثل عدم الترابط بين name و age. أنت كإنسان تدرك أنها مجرد ثلاثة أمثلة لشيء واحد، ولكننا لم نكن نملك حتى الآن وسيلة لكتابة "عدة أشياء من نفس النوع" في الكود.

القائمة (List) هي الحل الأمثل لذلك. اسم واحد، يحتوي على أي عدد تريده من القيم مرتبة داخله.

في نهاية هذا الدرس ستكون قادراً على

  • إنشاء قائمة والوصول إلى عناصرها باستخدام الفهارس والشرائح
  • تعديل القائمة باستخدام الدوال append و insert و remove و pop
  • شرح لماذا يمكن تعديل القائمة بينما لا يمكن تعديل النص
  • التعرف على فخ b = a وتجنبه (ربط اسمين بقائمة واحدة)
  • فهم رسائل الخطأ IndexError و ValueError ومعرفة أسبابها

المتطلبات السابقة: حلقات for ودالة range — التكرار لعدد محدد من المرات.


إنشاء قائمة

تُكتب القوائم باستخدام الأقواس المربعة، مع الفصل بين القيم بفواصل:

python
prices = [15, 60, 850]

print(prices)
print(len(prices))
text
[15, 60, 850]
3

تعمل دالة len() هنا تماماً كما كانت تعمل مع النصوص — لدينا ثلاث قيم، لذا تعيد 3.

طباعة القائمة تعرضها بالكامل بأقواسها ومحتوياتها. هذا الشكل ليس موجهاً للمستخدم النهائي ليقرأه، بل موجّه لك كمبرمج لتفهم البنية: فالناتج [15, 60, 850] يخبرك بوضوح أن هذه قائمة، وليس مجرد العدد 925.

الوصول إلى العناصر

تعمل الفهرسة (Indexing) بنفس الطريقة تماماً كما كانت في النصوص — يبدأ العدّ من الصفر، والأرقام السالبة تبدأ العد من النهاية إلى الخلف:

python
prices = [15, 60, 850]

print(prices[0])
print(prices[-1])
print(prices[1:])
text
15
850
[60, 850]

الأسطر الثلاثة تتبع قواعد الدرس السادس ذاتها دون تغيير. وهناك فارق واحد يستحق الملاحظة: التعبير prices[0] يعيد رقماً، بينما التعبير prices[1:] يعيد قائمة. فشريحة الاقتطاع تُرجع دائماً كائناً من نفس النوع الذي اقتُطعت منه، ولا ترجع عنصراً مفرداً مجرداً.

وهنا يكمن الفارق الحقيقي: القائمة قابلة للتعديل

في الدرس السادس رأينا أنه لا يمكن استبدال حرف داخل نص:

python
name = "rafi"
name[0] = "R"
text
TypeError: 'str' object does not support item assignment

نفس هذا السطر يعمل بسلاسة وبشكل مباشر على القوائم:

python
prices = [15, 60, 850]
prices[0] = 20
print(prices)
text
[20, 60, 850]

لم يتم إنشاء قائمة جديدة — بل تغيرت القائمة نفسها. هذه هي الخاصية الأهم على الإطلاق في القوائم، والفخ المذكور في نهاية هذا الدرس ناتج عنها مباشرة.

المصطلح البرمجي الدقيق لذلك هو: النصوص غير قابلة للتعديل (immutable)، بينما القوائم قابلة للتعديل (mutable).

الإضافة والحذف

python
stock = ["pen", "notebook"]

stock.append("bag")
stock.insert(1, "eraser")
print(stock)

stock.remove("pen")
print(stock)

last = stock.pop()
print(last)
print(stock)
text
['pen', 'eraser', 'notebook', 'bag']
['eraser', 'notebook', 'bag']
bag
['eraser', 'notebook']

أربع دوال لأربع مهام مختلفة:

  • append(x) — تضيف العنصر في نهاية القائمة
  • insert(i, x) — تضع العنصر في الفهرس i، وتزيح كل العناصر التي تليه خطوة إلى اليمين
  • remove(x) — تبحث عن أول عنصر يطابق تلك القيمة وتحذفه
  • pop() — تحذف العنصر الأخير من القائمة وتعيده لك لتستخدمه

الفارق بين هاتين الدالتين الأخيرتين هو مصدر معظم الارتباك: فدالة remove تريد معرفة ما الذي تريد حذفه بالاسم/القيمة، بينما pop تهتم بـ المكان/الفهرس الذي تريد الحذف منه. كما أن pop تسلّمك العنصر المحذوف لتخزنه في متغير، بينما remove لا تُرجع شيئاً.

هذه الدوال لا تُرجع القائمة. مع النصوص، كان عليك الاحتفاظ بنتيجة name.upper() لأنها تُنشئ نصاً جديداً وتعيده. أما القوائم فتعمل بالعكس: دالة append تعدل القائمة الأصلية نفسها مباشرةً وتُرجع None.

وهذا يقودنا إلى خطأ كلاسيكي شهير:

python
stock = ["pen", "notebook"]
stock = stock.append("bag")
print(stock)
text
None

لقد فُقدت القائمة تماماً! اكتب دائماً stock.append("bag")، ولا تكتب أبداً stock = stock.append("bag").

البحث والتكرار

python
stock = ["pen", "notebook", "bag"]

print("pen" in stock)
print("chair" in stock)

for item in stock:
    print(item.upper())
text
True
False
PEN
NOTEBOOK
BAG

المعامل in كان يبحث داخل النصوص، وهو يبحث داخل القوائم أيضاً — نفس الكلمة تؤدي نفس الوظيفة تماماً.

والحلقة for item in stock تمر على العناصر واحداً تلو الآخر. لاحظ أنه لا توجد دالة range ولا حسابات فهارس، تماماً كما كنا نمر على أحرف الكلمة في الدرس الحادي عشر.

وعندما تحتاج إلى موضع الفهرس والقيمة معاً، استخدم enumerate:

python
stock = ["pen", "notebook"]
for position, item in enumerate(stock, start=1):
    print(position, item)
text
1 pen
2 notebook

من دون start=1 سيبدأ العدّ من الصفر. وعند عرض قائمة للمستخدم، نفضل عادة أن يبدأ الترقيم من الرقم واحد.


الفخ الذي يقع فيه الجميع

في الدرس الرابع رأينا هذا المثال:

python
a = 3
b = a
b = b + 1
print(a, b)
text
3 4

استلم b قيمة مستقلة ولم يكن يراقب ما يحدث لـ a. الآن جرب فعل الشيء نفسه مع القوائم:

python
a = [1, 2, 3]
b = a
b.append(4)

print(a)
print(b)
text
[1, 2, 3, 4]
[1, 2, 3, 4]

تغير b، وتغير a معه أيضاً!

السبب لا يناقض القاعدة السابقة، بل هو القاعدة السابقة بعينها. في السطر b = a، استلم b تماماً ما كان يحمله a — وما كان يحمله a هو عنوان القائمة ومكانها في الذاكرة، وليس نسخة جديدة من القائمة. وبالتالي أصبح كلا الاسمين يشيران إلى القائمة نفسها. وحين أضاف الأمر b.append(4) عنصراً لتلك القائمة الوحيدة، أظهر كلا الاسمين نفس التغيير.

لن ترى هذا السلوك مع الأرقام أبداً، لأن السطر b = b + 1 لا يعدل الرقم القديم — بل يُنشئ رقماً جديداً ويضعه في b. الأرقام آمنة لأنها غير قابلة للتعديل. أما القوائم فقابلة للتعديل، لذا تتطلب الحذر.

إذا كنت تريد نسخة منفصلة ومستقلة، يجب عليك طلب ذلك صراحة:

python
a = [1, 2, 3]
b = a.copy()
b.append(4)

print(a)
print(b)
text
[1, 2, 3]
[1, 2, 3, 4]
متى يجب أن تنتبه؟ في أي وقت تضع فيه قائمة تحت اسم متغير ثانٍ، أو تمرر قائمة إلى دالة (كما سنرى في الدرس التاسع عشر)، اسأل نفسك مرة واحدة: هل أريد تعديل هذه القائمة الأصلية نفسها، أم أريد نسخة مستقلة عنها؟ المبرمجون الذين لا يطرحون هذا السؤال على أنفسهم يقضون أياماً طويلة في تتبع أخطاء غامضة تحت عنوان "لكنني لم أغير أي شيء هنا!".

مثال متكامل

shopping.py:

python
# A shopping list, built up and then summarised
items = []

items.append("pen")
items.append("notebook")
items.append("bag")
items.insert(0, "eraser")

print("List    :", items)
print("Count   :", len(items))
print("First   :", items[0])
print("Last    :", items[-1])
print("Has bag :", "bag" in items)

items.remove("pen")
dropped = items.pop()
print()
print("After removing 'pen' and popping the last one:")
print("List    :", items)
print("Dropped :", dropped)
print("Count   :", len(items))

print()
print("Numbered:")
for position, item in enumerate(items, start=1):
    print(f"  {position}. {item}")
text
List    : ['eraser', 'pen', 'notebook', 'bag']
Count   : 4
First   : eraser
Last    : bag
Has bag : True

After removing 'pen' and popping the last one:
List    : ['eraser', 'notebook']
Dropped : bag
Count   : 2

Numbered:
  1. eraser
  2. notebook

ثلاث ملاحظات جديرة بالاهتمام:

البدء بقائمة فارغة. السطر items = [] يؤدي نفس الدور الذي كان يقوم به المجمّع خارج الحلقة في الدرس العاشر — فكما وضعنا total = 0 سابقاً، نضع الآن []. فأنت بحاجة إلى وعاء تجميع قبل أن تبدأ في جمع العناصر.

الأمر insert(0, "eraser") يضع العنصر في المقدمة، ولذلك ظهرت eraser كأول عنصر في القائمة المطبوعة. فالترتيب الذي أُضيفت به العناصر ليس بالضرورة هو الترتيب الذي تستقر عليه.

الدالة pop() أعادت القيمة المحذوفة، لذا خزن المتغير dropped القيمة bag. بينما الدالة remove("pen") لم تُرجع شيئاً، ولذا اختفى القلم pen تماماً.


حالات الخطأ الشائعة

IndexError: list index out of range استخدام فهرس يتجاوز عدد العناصر الموجودة في القائمة. في قائمة مكونة من ثلاثة عناصر، الفهارس الصالحة هي 0 و 1 و 2 فقط — ولا يوجد prices[3]. وللوصول إلى العنصر الأخير بأمان، يُعد prices[-1] الخيار الأفضل لأنه لا يتطلب معرفة طول القائمة.

ValueError: list.remove(x): x not in list القيمة التي طلبت حذفها غير موجودة بالقائمة. تحقق أولاً باستخدام if x in stock:، أو استخدم try التي سنتعلمها في الدرس الرابع والعشرين.

AttributeError: 'NoneType' object has no attribute 'append' أحد الأسطر السابقة كتب خطأً stock = stock.append(...)، مما جعل stock يحمل القيمة None. لا تخزن النتيجة — فاستدعاء stock.append(...) بمفرده هو العملية الكاملة.

عدّلت قائمة واحدة فتغيرت قائمة أخرى معها اسمان مرتبطان بنفس القائمة في الذاكرة. اكتب b = a.copy() بدلاً من b = a.

أحذف عناصر داخل حلقة تكرارية ويتم تخطي بعض العناصر

python
numbers = [1, 2, 2, 3, 4]

for n in numbers:
    if n % 2 == 0:
        numbers.remove(n)

print(numbers)
text
[1, 2, 3]

لاحظ أن أحد أرقام 2 نجا ولم يُحذف! إن تقصير القائمة أثناء الدوران عليها يجعل عداد بايثون الداخلي غير متزامن مع مواضع القائمة الفعلية، مما يؤدي لتخطي بعض العناصر. والحل ليس بالحذف المباشر أثناء الدوران، بل بتجميع العناصر التي تريد الاحتفاظ بها في قائمة جديدة:

python
numbers = [1, 2, 2, 3, 4]
kept = []

for n in numbers:
    if n % 2 != 0:
        kept.append(n)

print(kept)
text
[1, 3]

سمّيت متغيراً باسم list ولم تعد دالة list(...) تعمل الاسم list هو أحد الأسماء المحجوزة المدمجة في بايثون. استخدم دائماً أسماء دالة ومعبرة — مثل items أو prices أو names.