اشتقاق المجموعات والقوائم — Comprehensions
اشتقاق القوائم والقواميس والمجموعات، الفرق بين if للتصفية و if/else للاختيار، ومتى يكون اشتقاق القوائم خياراً خاطئاً.
كيف تريد أن تتعلم؟
المشكلة التي نقوم بحلها
ظل نمط برمجي واحد يتكرر معنا باستمرار عبر الفصول الستة الماضية:
marks = [72, 45, 90, 33]
doubled = []
for mark in marks:
doubled.append(mark * 2)
print(doubled)
doubled = [mark * 2 for mark in marks]
print(doubled)[144, 90, 180, 66]
[144, 90, 180, 66]الأسطر الأربعة في الجزء العلوي والسطر الواحد في الجزء السفلي يقومان بنفس المهمة تماماً.
ثلاثة أسطر من تلك الأربعة ليست العمل الحقيقي، بل مجرد إجراءات روتينية — إنشاء قائمة فارغة، الدوران على المصدر، وإضافة العنصر في كل دورة. القرار الحقيقي الوحيد هو mark * 2. وكل ما عدا ذلك يتكرر بنفس الطريقة في كل مرة.
تأتي صيغ الاشتقاق أو التوليد (Comprehensions) لتتخلص من هذا التكرار الروتيني. إنها ليست قدرة جديدة — بل هي اختصار لما كنت تكتبه بالفعل، ولكن بكلمات أقل. ومن هنا أيضاً ينبع القيد الأهم لها، وهو موضوع القسم الأخير من هذا الفصل: اختصر فقط ما يصبح أكثر وضوحاً باختصاره، ولا تختصر كل شيء لمجرد الاختصار.
في نهاية هذا الفصل ستكون قادراً على
- كتابة وقراءة اشتقاق القوائم (list comprehension)
- تصفية العناصر باستخدام
if، والاختيار بين قيمتين باستخدامif/else - كتابة اشتقاق القواميس والمجموعات (Dict & Set Comprehensions)
- تحديد متى يكون استخدام الاشتقاق خياراً غير مناسب
- تفكيك تعبير اشتقاق معقد وغير مقروء وإعادته إلى حلقة تكرارية واضحة
المتطلبات السابقة: البيانات المتداخلة — قوائم القواميس.
البنية الهيكلية
[ result for item in source ]هنا result هو ما تريد بناءه، و item عنصر واحد في كل دورة، و source هو القائمة (أو أي تجميع آخر) التي تأتي منها العناصر.
عند قراءتها بصوت عالٍ، تبدو كجملة طبيعية: "`mark 2 لكل mark في marks`"*.
وأفضل طريقة لفهمها: اقرأ جزء for أولاً، ثم اقرأ البداية. فمعرفة مصدر البيانات أولاً تجعل من السهل جداً استيعاب ما يتم بناؤه منها.
التصفية والفلترة
إضافة شرط if في النهاية يحتفظ فقط بالعناصر التي تحقق هذا الشرط:
marks = [72, 45, 90, 33]
passed = [mark for mark in marks if mark >= 40]
print(passed)[72, 45, 90]هنا، الجزء الأول هو مجرد mark — دون أي عمليات حسابية، يؤخذ العنصر كما هو. ويمكن الجمع بين التصفية وتعديل البيانات في وقت واحد:
names = [" rafi ", "AHMED", " dia"]
clean = [name.strip().lower() for name in names if name.strip()]
print(clean)['rafi', 'ahmed', 'dia']الشرط هنا هو name.strip() دون أي مقارنة صريحة. تذكر قاعدة الفصل 8 حول القيم المنطقية: السلسلة النصية (string) الفارغة تُعامل كقيمة خاطئة (False). ولذلك فإن أي شخص أدخل مسافات فارغة فقط يتم استبعاده تلقائياً.
وجود if في النهاية مقابل وجودها في البداية
هذان أمران مختلفان تماماً، وموضع if هو الفارق الحاسم:
marks = [72, 33, 90]
labels = ["pass" if m >= 40 else "fail" for m in marks]
print(labels)['pass', 'fail', 'pass']وجود if في النهاية يقوم بالتصفية (Filtering) — بعض العناصر لا تصل إلى النتيجة النهائية أبداً، وبالتالي يمكن أن تكون القائمة الناتجة أقصر طولاً من الأصلية.
أما صيغة if ... else في البداية فتقوم بالاختيار (Choosing) — كل عنصر من المصدر ينتج عنه بالضرورة عنصر في النتيجة، وبالتالي يظل الطول ثابتاً كما هو. هذه هي العبارة الشرطية ذات السطر الواحد التي رأيناها في الفصل 15، مستخدمة داخل تعبير الاشتقاق.
هل سيتغير عدد العناصر؟ الإجابة عن هذا السؤال هي أسهل طريقة للتمييز بين الحالتين فوراً.
اشتقاق القواميس والمجموعات أيضاً
نفس البنية المنطقية، بأقواس مختلفة:
marks = {"rafi": 72, "ahmed": 45, "bilal": 90}
passed = {name: mark for name, mark in marks.items() if mark >= 50}
print(passed)
doubled = {name: mark * 2 for name, mark in marks.items()}
print(doubled){'rafi': 72, 'bilal': 90}
{'rafi': 144, 'ahmed': 90, 'bilal': 180}استخدام الأقواس المعقوفة مع النقطتين الرأسيتين ينشئ اشتقاقاً للقاموس (dictionary comprehension). تعيد دالة marks.items() صفاً (tuple) في كل دورة وتفكك العبارة name, mark حزمته، تماماً كما في الحلقة التكرارية.
ونفس الأقواس المعقوفة ولكن بدون نقطتين رأسيتين تنشئ مجموعة (set):
orders = [
{"item": "pen"},
{"item": "bag"},
{"item": "pen"},
]
items = {order["item"] for order in orders}
print(sorted(items))['bag', 'pen']سؤال الفصل 16 "كم عنصراً مختلفاً" أُجيب عنه هنا في سطر واحد. وصل المنتج pen مرتين وظهر في النتيجة مرة واحدة فقط.
تحويل الأرقام إلى نصوص — وهو ما رأيته بالفعل
ظهر هذا السطر في الفصل 13 مع وعد بأن الشرح التفصيلي قادم:
marks = [72, 45, 90]
print(", ".join(str(m) for m in marks))
print([str(m) for m in marks])72, 45, 90
['72', '45', '90']الآن أصبح بإمكانك قراءته وفهمه بالكامل. التعبير str(m) for m in marks هو اشتقاق بدون أقواس مربعة — وتسمى هذه الصيغة تعبير المولد (generator expression)، وبدلاً من بناء القائمة بأكملها وحجزها في الذاكرة دفعة واحدة، فإنها تسلّم دالة join عنصراً واحداً في كل مرة. ويمكن الاستغناء عن الأقواس تماماً عندما يكون هذا التعبير هو الوسيط الوحيد الممرر للدالة، وهذا يوفر الذاكرة مع البيانات الكبيرة.
متى يجب أن تتجنب كتابة تعابير الاشتقاق؟
مجرد قدرتك على كتابة تعبير اشتقاق مضغوط لا يعني أنك يجب أن تفعل ذلك دائماً. تأمل هذا الكود:
rows = [["1", "2"], ["3", "x"]]
numbers = [int(c) for row in rows for c in row if c.isdigit()]
print(numbers)[1, 2, 3]هذا السطر صحيح برمجياً ويعمل تماماً. لكنه يحتوي على عبارتي for وشرط if، ويجبرك على تذكر الترتيب: فعبارات for تُقرأ من الخارجية إلى الداخلية، تماماً كما لو كانت حلقات متداخلة. وبعد ستة أشهر، سيكون فهم هذا السطر صعباً.
انظر إليه بصيغة الحلقة التكرارية العادية:
rows = [["1", "2"], ["3", "x"]]
numbers = []
for row in rows:
for c in row:
if c.isdigit():
numbers.append(int(c))
print(numbers)[1, 2, 3]أسطر أكثر، لكن بجهد تفكير أقل بكثير. الهدف من كتابة الكود ليس أن يكون قصيراً، بل أن يكون واضحاً ومقروءاً — وفي أغلب الأحيان يجعل الاختصار الكود أكثر وضوحاً، ولكن ليس دائماً.
كقاعدة عمل عملية: عبارة for واحدة وشرط if واحد على الأكثر — استخدم تعبير اشتقاق. أكثر من ذلك — استخدم حلقة تكرارية عادية. وإياك أبداً أن تضع print أو append داخل تعبير اشتقاق؛ فهو مخصص لبناء قيمة جديدة، وليس للقيام بإجراءات وأفعال جانبية.
مثال متكامل
views.py:
# One list of records in, three different views out
orders = [
{"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
{"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
{"customer": "rafi", "item": "ink", "quantity": 2, "price": 120.0},
{"customer": "dia", "item": "pen", "quantity": 5, "price": 15.0},
]
totals = [order["quantity"] * order["price"] for order in orders]
print("Totals :", totals)
big = [order["item"] for order in orders if order["quantity"] * order["price"] > 100]
print("Over 100 :", big)
items = {order["item"] for order in orders}
print("Distinct :", sorted(items))
by_item = {order["item"]: order["price"] for order in orders}
print("Price list:", by_item)
print("Grand :", sum(totals))Totals : [45.0, 850.0, 240.0, 75.0]
Over 100 : ['bag', 'ink']
Distinct : ['bag', 'ink', 'pen']
Price list: {'pen': 15.0, 'bag': 850.0, 'ink': 120.0}
Grand : 1210.0هذه هي نفس البيانات من الفصل 17، وقد تحول معظم ما كان حلقات تكرارية هناك إلى أسطر مفردة هنا.
ثلاث ملاحظات بارزة:
كُتبت totals و sum(totals) في سطرين منفصلين. كان بإمكاننا اختصار المجموع في sum(order["quantity"] * order["price"] for order in orders)، ولكن حينها لم تكن قيم كل طلب على حدة لتوجد في متغير. إعطاء خطوة وسيطة اسماً دالاً غالباً ما يكون أكثر فائدة وقيمة من حيلة برمجية في سطر واحد.
تكررت نفس العملية الحسابية مرتين داخل big — مرة في الشرط، وكان يمكن أن تُطلب مرة أخرى في النتيجة. هنا النتيجة أخذت الاسم فقط فلم نخسر شيئاً؛ ولكن عندما تحتاج إلى كلا الأمرين، فهذا سبب وجيه جداً للتخلي عن الاشتقاق وكتابة حلقة عادية، حيث يمكن حساب القيمة مرة واحدة وتخزينها في متغير.
فقد القاموس by_item سجلاً. أربعة طلبات، وثلاثة أزواج فقط في النتيجة — تكرر المنتج pen مرتين، وبحكم قاعدة الفصل 15 استبدلت القيمة اللاحقة القيمة السابقة بهدوء. هنا كان للمنتجين نفس السعر فلم يحدث ضرر؛ لكن لو كانت الأسعار مختلفة لكان الكود خاطئاً في صمت. لا يخلق الاشتقاق هذه المشكلة بنفسه، لكن إيجازه يقلل من فرصة ملاحظة الخطأ.
حالات الخطأ الشائعة
خطأ SyntaxError بسبب وضع if و else في نهاية التعبير التعبير [m for m in marks if m >= 40 else 0] غير صالح نحوياً. فشرط if الخاص بالتصفية يوضع في النهاية ولا يحتوي على else أبداً؛ بينما صيغة الاختيار if ... else توضع في المقدمة: [m if m >= 40 else 0 for m in marks].
القائمة الناتجة مليئة بقيم None هناك أمر بداخل الاشتقاق لا يُرجع أي قيمة — مثل [items.append(x) for x in other]. تعيد دالة append القيمة None، ولذا تصبح النتيجة قائمة من None. تهدف تعابير الاشتقاق إلى بناء قيم؛ وللقيام بأفعال جانبية، اكتب حلقة تكرارية عادية.
خطأ NameError — متغير الاشتقاق غير موجود خارجه بعد تنفيذ [m * 2 for m in marks] لا يمكنك استخدام المتغير m. في الفصل 11 رأينا أن متغير حلقة for يبقى حياً بعد الحلقة؛ أما في تعابير الاشتقاق فلا يبقى — ونطاق حياته محصور داخل التعبير فقط.
أنتج اشتقاق القاموس أزواجاً أقل مما هو متوقع تم إنتاج نفس المفتاح أكثر من مرة فاستبدلت القيمة الأحدث ما قبلها. قارن الأطوال دائماً باستخدام len().
لا أستطيع قراءة وفهم السطر بسهولة هذا بحد ذاته سبب كافٍ تماماً. فكك السطر إلى حلقة تكرارية عادية — فرق السرعة نادراً ما يهم في مرحلة التعلم، والشخص التالي الذي سيقرأ الكود (وهو أنت على الأرجح) سيكون ممتناً لك.
الخطوة ٤ من ٦ — التوقع
تحقّق من فهمك
يوجد if في النهاية. ماذا يُطبع؟
marks = [72, 45, 90, 33]
passed = [mark for mark in marks if mark >= 40]
print(passed)- A[72, 45, 90, 33]
- B[True, True, True, False]
- C[33]
- D[72, 45, 90]
هذه المرة if في البداية ومعه else. ماذا يُطبع؟
marks = [72, 33, 90]
labels = ["pass" if m >= 40 else "fail" for m in marks]
print(labels)- A['pass', 'fail', 'pass']
- B['pass', 'pass']
- C[72, 90]
- D['pass', 'fail']
استُدعي append داخل اشتقاق (comprehension). ماذا يطبع السطران؟
source = ["pen", "bag"]
target = []
result = [target.append(item) for item in source]
print(target)
print(result)- A['pen', 'bag'] [None, None]
- Bخطأ `TypeError`
- C['pen', 'bag'] ['pen', 'bag']
- D[] ['pen', 'bag']
الإجابة تتطلب حساباً
سجّل الدخول للتحقق من إجاباتك
الأسئلة موجودة أعلاه، والتفكير في إجاباتها ذهنياً هو الجزء المهم. سجّل الدخول لرؤية الإجابات والتفسيرات والتلميحات ذات المستويات الثلاثة.
دورك الآن
اكتب ملفاً باسم clean.py يحتوي على قائمة بأسماء غير مرتبة — بعضها يحتوي على مسافات زائدة، وبعضها مكتوب بحروف كبيرة، واسم واحد على الأقل فارغ تماماً من الحروف.
باستخدام تعابير الاشتقاق (comprehensions)، قم ببناء:
- قائمة بالأسماء النظيفة — بعد إزالة الفراغات وتحويلها لحروف صغيرة وحذف الأسماء الفارغة
- قائمة بأطوال كل اسم من الأسماء
- قائمة بالأسماء التي يزيد طولها عن خمسة أحرف فقط
- قاموس يربط كل اسم بطوله:
{name: length} - مجموعة (set) تضم الحرف الأول من كل اسم
ثم أجرِ هاتين التجربتين:
- أعد كتابة المطلب الأول كحلقة تكرارية عادية
for، وتأكد من تطابق النتيجتين باستخدام المعامل== - اكتب تعبير اشتقاق يحتوي على حلقتي
forوشرطif، ثم فككه إلى حلقة تكرارية عادية. أيهما أسهل في القراءة والفهم؟
هذا السؤال الأخير لا يملك إجابة "صحيحة" واحدة مطلقة، وهذا هو الهدف الأساسي منه. فتعلّم طرح هذا السؤال هو الدرس الحقيقي لهذا الفصل.
الخطوة ٦ من ٦
التحدي — اختبار الفصل
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
سجّل الدخول لأداء الاختبار