الفصل 13

ترتيب القوائم ودوالها المدمجة

الفرق بين sorted() و .sort()، والنتائج التي تقدمها max و min و sum و count و index، وتغيير معايير الترتيب باستخدام key=، وتحويل القائمة لنص قابل للقراءة بواسطة join().

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

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

لدينا قائمة تحتوي على درجات سبعة طلاب. ونحتاج إلى إجابات لثلاثة أسئلة: ما هي أعلى درجة؟ ما هو المتوسط الحسابي؟ ومن هم أصحاب أعلى ثلاث درجات؟

باستخدام الأدوات التي تعلمناها في الدرس الثاني عشر، يمكننا الإجابة عن أول سؤالين باستخدام حلقة تكرارية — وهو أمر يتطلب بعض الجهد ولكنه ممكن تماماً. أما السؤال الثالث فمختلف تماماً؛ فمعرفة "أعلى ثلاث درجات" يتطلب ترتيب الدرجات أولاً، ونحن لا نملك حتى الآن طريقة لترتيب أي شيء في الكود.

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

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

  • التمييز بين sorted() و .sort()، ومعرفة متى تستخدم كلاً منهما
  • استخراج النتائج من القائمة باستخدام max و min و sum و count و index
  • تغيير معيار الترتيب وقواعده باستخدام وسيط key=
  • تحويل القائمة إلى نص منظم وقابل للقراءة باستخدام join()
  • شرح لماذا يتسبب ترتيب قائمة تحتوي على أنواع بيانات مختلطة في إطلاق خطأ TypeError

المتطلبات السابقة: القوائم — قيم متعددة تحت اسم واحد.


sorted() و .sort() — جوهر هذا الدرس

كلاهما يقوم بمهمة الترتيب، لكن الفارق الجوهري يكمن في النتيجة المُعادة.

python
marks = [72, 45, 90, 61]

ordered = sorted(marks)
print(ordered)
print(marks)

marks.sort()
print(marks)
text
[45, 61, 72, 90]
[72, 45, 90, 61]
[45, 61, 72, 90]

اقرأ أسطر المخرجات الثلاثة بعناية فائقة.

الدالة sorted(marks) أنشأت قائمة جديدة مرتبة وسلمتها لك. أما القائمة الأصلية marks فقد بقيت دون أي تعديل — والسطر الثاني في المخرجات هو الدليل القاطع على ذلك.

أما الطريقة marks.sort() فلم تنشئ أي قائمة جديدة، بل قامت بترتيب القائمة الأصلية نفسها في مكانها، وضاع الترتيب القديم للأبد.

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

python
marks = [72, 45, 90, 61]
marks = marks.sort()
print(marks)
text
None

تماماً كما حدث مع دالة append. مع .sort() يجب ألا تخزن النتيجة في متغير؛ بينما مع sorted() يجب عليك تخزين النتيجة، وإلا ضاع مجهود الترتيب هباءً.

أيهما تختار ومتى؟ إذا لم تعد بحاجة إلى الترتيب الأصلي، فإن .sort() أبسط وأخف استهلاكاً للذاكرة. أما إذا كانت القائمة الأصلية مستخدمة في مكان آخر، أو لم تكن متأكداً، فاستخدم sorted() — فهي آمنة ولا تمس القائمة الأصلية بسوء. وعند الشك، اختر sorted().

الترتيب العكسي والنتائج الرقمية

python
marks = [72, 45, 90, 61]

print(sorted(marks, reverse=True))
print(max(marks), min(marks), sum(marks))
print(round(sum(marks) / len(marks), 2))
text
[90, 72, 61, 45]
90 45 268
67.0

الدوال max و min و sum هي دوال عامة مدمجة وليست دوال تابعة (methods)، لذا نكتب max(marks) وليس marks.max(). ولا توجد دالة مدمجة خاصة بالمتوسط الحسابي: فالمجموع مقسوماً على العدد الإجمالي هو المتوسط بعينه، وتأتي round(..., 2) لتقريب الناتج إلى منزلتين عشريتين للعرض.

لاحظ كيف اختُصرت حلقة التجميع التكرارية من الدرس العاشر في سطر واحد أنيق. ومع ذلك، لم يكن تعلم الحلقة مضيعة للوقت — فما تفعله sum داخلياً هو تلك الحلقة التكرارية تماماً، وسيأتي يوم تحتاج فيه إلى تجميع قيم لا توفر بايثون دالة جاهزة لها.

العد والبحث عن المواقع

python
marks = [72, 45, 90, 45, 61]

print(marks.count(45))
print(marks.index(45))
print(marks.index(90))
text
2
1
2

تخبرك count(x) بعدد مرات ظهور القيمة x. وتخبرك index(x) بموقع الفهرس لأول ظهور للعنصر — فعند وجود رقمين 45، ترجع الفهرس 1 ولا تذكر الظهور الثاني على الإطلاق.

والبحث عن عنصر غير موجود يوقف البرنامج ويطلق خطأ:

python
marks = [72, 45, 90]
print(marks.index(50))
text
ValueError: 50 is not in list

تماماً كما رأينا مع دالة remove. عوّد نفسك دائماً على الفحص المسبق باستخدام if 50 in marks: أولاً.

ترتيب النصوص والوسيط key=

python
names = ["rafi", "Bilal", "ahmed"]

print(sorted(names))
print(sorted(names, key=str.lower))
text
['Bilal', 'ahmed', 'rafi']
['ahmed', 'Bilal', 'rafi']

عندما يرى البعض النتيجة الأولى يظنون أن بايثون أخطأت. لكنها لم تخطئ — فهي ترتب النصوص بناءً على الترميز الرقمي للأحرف (character codes)، وفي هذا الترميز تسبق جميع الحروف الكبيرة (Capital) نظيراتها الصغيرة (lowercase). لذلك قفز الاسم Bilal إلى المقدمة.

أما الوسيط key=str.lower فيخبر بايثون: أثناء عملية الترتيب والمقارنة، عاملي كل اسم وكأنه مكتوب بحروف صغيرة. لاحظ أن الأسماء نفسها لم تتغير — فالاسم Bilal لا يزال يطبع بحرفه الكبير B — فقط عملية المقارنة هي التي استخدمت صيغة الحروف الصغيرة.

يمكن أن يكون الوسيط key أي معيار أو قاعدة تريدها:

python
words = ["bag", "pen", "notebook", "ink"]
print(sorted(words, key=len))
text
['bag', 'pen', 'ink', 'notebook']

تم الترتيب هنا حسب الطول وعدد الأحرف. لاحظ أن الكلمات bag و pen و ink تتكون جميعها من ثلاثة أحرف، وقد بقيت بنفس الترتيب الذي كانت عليه أصلاً — فبايثون لا تعبث بترتيب العناصر المتساوية في القيمة. وتسمى هذه الخاصية الترتيب المستقر (stable sort)، وهي ما يجعل الترتيب وفق معايير متعددة أمراً ممكناً ودقيقاً.

لا يمكن ترتيب الأنواع المختلطة

python
values = [3, "1", 2]
print(sorted(values))
text
TypeError: '<' not supported between instances of 'str' and 'int'

الترتيب يعني في جوهره مقارنة العناصر زوجاً بزوج، وقد رأينا في الدرس الثامن أنه لا يمكن مقارنة رقم بنص باستخدام معامل الأصغر من <. نص رسالة الخطأ هنا مطابق تماماً لما رأيناه سابقاً. يتكرر هذا الخطأ باستمرار عند قراءة القوائم من ملفات CSV، حيث يُقرأ كل رقم كنص في البداية.

من قائمة إلى نص — دالة join

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

python
names = ["rafi", "ahmed", "bilal"]

print(", ".join(names))
print(" | ".join(names))
text
rafi, ahmed, bilal
rafi | ahmed | bilal

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

هناك شرط واحد وصارم للغاية: يجب أن تكون جميع العناصر نصوصاً.

python
marks = [72, 45, 90]
print(", ".join(marks))
text
TypeError: sequence item 0: expected str instance, int found

لربط قائمة من الأرقام، حوّل كل رقم فيها إلى نص أولاً:

python
marks = [72, 45, 90]
print(", ".join(str(m) for m in marks))
text
72, 45, 90

العبارة str(m) for m in marks هي موضوع الدرس الثامن عشر — اقرأها الآن ببساطة على أنها تعني: "طبق دالة str() على كل قيمة".


مثال متكامل

report.py:

python
# A marks report: order them, summarise them, and show the top three
marks = [72, 45, 90, 61, 88, 45, 33]

ordered = sorted(marks, reverse=True)

print("Marks    :", marks)
print("Ordered  :", ordered)
print("Count    :", len(marks))
print("Highest  :", max(marks))
print("Lowest   :", min(marks))
print("Total    :", sum(marks))
print("Average  :", round(sum(marks) / len(marks), 2))
print("How many 45s:", marks.count(45))

top_three = ordered[:3]
print("Top three:", top_three)
print("As text  :", ", ".join(str(m) for m in top_three))

passed = []
for mark in marks:
    if mark >= 40:
        passed.append(mark)

print("Passed   :", len(passed), "of", len(marks))
text
Marks    : [72, 45, 90, 61, 88, 45, 33]
Ordered  : [90, 88, 72, 61, 45, 45, 33]
Count    : 7
Highest  : 90
Lowest   : 33
Total    : 434
Average  : 62.0
How many 45s: 2
Top three: [90, 88, 72]
As text  : 90, 88, 72
Passed   : 6 of 7

يجدر التوقف عند قرارين تصميميين في هذا الكود:

تم تفضيل sorted() على .sort() لأن القائمة الأصلية marks يُعاد طباعتها مجدداً في السطر الأول. لو استخدمنا .sort()، لأظهر سطرا "Marks" و "Ordered" نفس الترتيب دون أن يصدر بايثون أي اعتراض أو تحذير. وهذا بالضبط هو الخطأ الصامت الذي افتتحنا به هذا الدرس.

تم استخراج أعلى ثلاث درجات باستخدام شريحة — ordered[:3]. لم نكن بحاجة إلى دالة خاصة لـ "أعلى ثلاثة": فالعناصر الثلاثة الأولى في قائمة مرتبة تنازلياً هي أعلى ثلاثة عناصر بالفعل. بمجرد ترتيب البيانات، تتحول الكثير من الأسئلة البرمجية المعقدة إلى مجرد شريحة واحدة بسيطة.


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

أصبحت قائمتي None بعد ترتيبها السطر كُتب بصيغة marks = marks.sort(). ترتب .sort() القائمة في مكانها وترجع None — اكتب فقط marks.sort(). وإذا أردت قائمة جديدة، استخدم ordered = sorted(marks).

ترتيب القائمة أدى إلى تغيير القائمة الأصلية أيضاً استخدمت .sort() التي ترتب في المكان نفسه. استخدم sorted() لترك القائمة الأصلية على حالها.

TypeError: '<' not supported between instances of 'str' and 'int' تحتوي القائمة على مزيج من الأرقام والنصوص. شغّل print(marks) وانظر أي القيم محاطة بعلامات اقتباس؛ وإذا كانت مقروءة من ملف فمن المرجح أنها نصوص كلها وتحتاج للتحويل بواسطة int().

ValueError: 50 is not in list مُررت إلى index() أو remove() قيمة لا تحتويها القائمة. تحقق أولاً باستخدام المعامل in.

TypeError: sequence item 0: expected str instance, int found مُررت إلى join() قائمة من الأرقام. يجب تغليف كل عنصر بدالة str().

بعد ترتيب الأسماء، جاءت جميع الأسماء المبتدئة بحرف كبير أولاً هذا سلوك طبيعي في لغة بايثون وليس خطأً برمجياً. للترتيب بالطريقة البشرية المعتادة، اكتب sorted(names, key=str.lower).