اشتقاق المجموعات والقوائم — Comprehensions
اشتقاق القوائم والقواميس والمجموعات، الفرق بين if للتصفية و if/else للاختيار، ومتى يكون اشتقاق القوائم خياراً خاطئاً.
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
ظل نمط برمجي واحد يتكرر معنا باستمرار عبر الدروس الستة الماضية:
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)
- تحديد متى يكون استخدام الاشتقاق خياراً غير مناسب
- تفكيك تعبير اشتقاق معقد وغير مقروء وإعادته إلى حلقة تكرارية واضحة
المتطلبات السابقة: البيانات المتداخلة — قوائم القواميس.
البنية الهيكلية
[ what I want for each thing in where it comes from ]عند قراءتها بصوت عالٍ، تبدو كجملة طبيعية: "`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() دون أي مقارنة صريحة. تذكر قاعدة الدرس الثامن حول القيم المنطقية: النص الفارغ يُعامل كقيمة خاطئة (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) — كل عنصر من المصدر ينتج عنه بالضرورة عنصر في النتيجة، وبالتالي يظل الطول ثابتاً كما هو. هذه هي العبارة الشرطية ذات السطر الواحد التي رأيناها في الدرس الخامس عشر، مستخدمة داخل تعبير الاشتقاق.
هل سيتغير عدد العناصر؟ الإجابة عن هذا السؤال هي أسهل طريقة للتمييز بين الحالتين فوراً.
اشتقاق القواميس والمجموعات أيضاً
نفس البنية المنطقية، بأقواس مختلفة:
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']سؤال الدرس السادس عشر "كم عنصراً مختلفاً" أُجيب عنه هنا في سطر واحد! وصل المنتج pen مرتين وظهر في النتيجة مرة واحدة فقط.
تحويل الأرقام إلى نصوص — وهو ما رأيته بالفعل
ظهر هذا السطر في الدرس الثالث عشر مع وعد بأن الشرح التفصيلي قادم:
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هذه هي نفس البيانات من الدرس السابع عشر، ولكن تحول معظم ما كان حلقات تكرارية هناك إلى أسطر مفردة أنيقة هنا.
ثلاث ملاحظات بارزة:
كُتبت totals و sum(totals) في سطرين منفصلين. كان بإمكاننا اختصار المجموع في sum(order["quantity"] * order["price"] for order in orders)، ولكن حينها لم تكن قيم كل طلب على حدة لتوجد في متغير. إعطاء خطوة وسيطة اسماً دالاً غالباً ما يكون أكثر فائدة وقيمة من حيلة برمجية في سطر واحد.
تكررت نفس العملية الحسابية مرتين داخل big — مرة في الشرط، وكان يمكن أن تُطلب مرة أخرى في النتيجة. هنا النتيجة أخذت الاسم فقط فلم نخسر شيئاً؛ ولكن عندما تحتاج إلى كلا الأمرين، فهذا سبب وجيه جداً للتخلي عن الاشتقاق وكتابة حلقة عادية، حيث يمكن حساب القيمة مرة واحدة وتخزينها في متغير.
فقد القاموس by_item سجلاً. أربعة طلبات، وثلاثة أزواج فقط في النتيجة — تكرر المنتج pen مرتين، وبحكم قاعدة الدرس الخامس عشر استبدلت القيمة اللاحقة القيمة السابقة بهدوء. هنا كان للمنتجين نفس السعر فلم يحدث ضرر؛ لكن لو كانت الأسعار مختلفة لكان الكود خاطئاً في صمت. لا يخلق الاشتقاق هذه المشكلة بنفسه، لكن إيجازه يقلل من فرصة ملاحظة الخطأ.
حالات الخطأ الشائعة
خطأ 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. في الدرس الحادي عشر رأينا أن متغير حلقة for يبقى حياً بعد الحلقة؛ أما في تعابير الاشتقاق فلا يبقى — ونطاق حياته محصور داخل التعبير فقط.
أنتج اشتقاق القاموس أزواجاً أقل مما هو متوقع تم إنتاج نفس المفتاح أكثر من مرة فاستبدلت القيمة الأحدث ما قبلها. قارن الأطوال دائماً باستخدام len().
لا أستطيع قراءة وفهم السطر بسهولة هذا بحد ذاته سبب كافٍ تماماً. فكك السطر إلى حلقة تكرارية عادية — فالكود لن يعمل ببطء، والشخص التالي الذي سيقرأ الكود (وهو أنت على الأرجح في المستقبل) سيكون ممتناً لك جداً.
Step 4 of 6 — Predict
Check your understanding
There is an if at the end. What is printed?
marks = [72, 45, 90, 33]
passed = [mark for mark in marks if mark >= 40]
print(passed)- A[72, 45, 90]
- B[72, 45, 90, 33]
- C[True, True, True, False]
- D[33]
This time the if is at the front, with an else. What is printed?
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 is called inside a comprehension. What do the two lines print?
source = ["pen", "bag"]
target = []
result = [target.append(item) for item in source]
print(target)
print(result)- A['pen', 'bag'] [None, None]
- B['pen', 'bag'] ['pen', 'bag']
- C[] ['pen', 'bag']
- DA `TypeError`
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
دورك الآن
اكتب ملفاً باسم clean.py يحتوي على قائمة بأسماء غير مرتبة — بعضها يحتوي على مسافات زائدة، وبعضها مكتوب بحروف كبيرة، واسم واحد على الأقل فارغ تماماً من الحروف.
باستخدام تعابير الاشتقاق (comprehensions)، قم ببناء:
- قائمة بالأسماء النظيفة — بعد إزالة الفراغات وتحويلها لحروف صغيرة وحذف الأسماء الفارغة
- قائمة بأطوال كل اسم من الأسماء
- قائمة بالأسماء التي يزيد طولها عن خمسة أحرف فقط
- قاموس يربط كل اسم بطوله:
{name: length} - مجموعة (set) تضم الحرف الأول من كل اسم
ثم أجرِ هاتين التجربتين:
- أعد كتابة المطلب الأول كحلقة تكرارية عادية
for، وتأكد من تطابق النتيجتين باستخدام المعامل== - اكتب تعبير اشتقاق يحتوي على حلقتي
forوشرطif، ثم فككه إلى حلقة تكرارية عادية. أيهما أسهل في القراءة والفهم؟
هذا السؤال الأخير لا يملك إجابة "صحيحة" واحدة مطلقة، وهذا هو الهدف الأساسي منه. فتعلم طرح هذا السؤال التقييمي هو الدرس الحقيقي لهذا الفصل.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz