النص الكامل للمحاضرة 13: الاستثناءات والتوكيدات
النص الكامل للمحاضرة 13: الاستثناءات والتوكيدات
المصدر الأصلي: النص الرسمي الإنجليزي، PDF، المحاضرة والفيديو الرسمي.
النسبة والترخيص: آنا بيل (Ana Bell)، مقرر 6.100L، خريف 2022، معهد ماساتشوستس للتكنولوجيا، MIT OpenCourseWare. هذه ترجمة وتكييف عربي غير رسمي للاستخدام غير التجاري، وليست معتمدة من MIT ولا تعني تأييده لها؛ تخضع المواد الأصلية المشمولة والترجمة لرخصة CC BY-NC-SA 4.0، مع وجوب نسبة المصدر ومشاركة التكييفات بالرخصة نفسها. لا تشمل الرخصة الافتراضية مواد الأطراف الثالثة المستثناة.
ملاحظة المترجم — المنهج: تتبع الترجمة ترتيب النص الرسمي وفقراته، بما في ذلك التكرار والتردد والتصحيحات الشفهية ومداخلات الحضور والإشارات الصوتية. أقسام «صفحة المصدر» تطابق الصفحات الإحدى عشرة غير الفارغة في النص المستخرج من PDF، وليست تقسيمًا موضوعيًّا. حُفظت أسماء Python والتعبيرات البرمجية والأعداد. نُقلت أمثلة الشرح دون تلخيص، وكل توضيح إضافي موسوم بملاحظة مترجم.
صفحة المصدر 1
MITOCW | المحاضرة 13 من 6.100L، الإصدار 2.mp4
[صرير]
[حفيف]
[نقرات]
آنا بيل (ANA BELL):
حسنًا. أهلًا بالجميع. إذا فاتتكم المحاضرة الماضية، فلدي بعض بطات تنقيح الأخطاء الإضافية التي بقيت من محاضرة التنقيح الماضية. أرجو أن تأخذوها إلى بيوتكم. لا أريد الاضطرار إلى إعادتها إلى مكتبي ثم إحضارها مرة أخرى مرات كثيرة.
أرجو إذًا أن تعطوها بيتًا جيدًا. إذا وجدتموها مفيدة في استراتيجية تنقيح الأخطاء (Debugging) طوال مسيرتكم البرمجية، أقترح أن تنتقلوا إلى حمل بطة تنقيح معكم في كل مكان، كما لدي على هاتفي. أستخدمها في حياتي اليومية. هذه فقط الخطوة التالية بعد البطة الفعلية.
حسنًا، لنبدأ محاضرة اليوم. سنتناول فكرة الاستثناءات (Exceptions) والتوكيدات (Assertions). وهذه أساسًا تلك الأخطاء الحمراء المخيفة التي نحصل عليها عندما يتعطل برنامجنا. حسنًا؟ آمل أن تسلط محاضرة اليوم بعض الضوء على ما هذه الاستثناءات بالضبط، وكيف نستطيع استخدامها فعلًا لصالحنا في الكود.
لنبدأ بالحديث عن الاستثناءات. عندما تشغّل الكود، فإنه عادة يعمل دون خطأ وينتج المخرج الصحيح طوال الوقت، كما يفعل كودي. لكن أحيانًا يحدث أن يصادف كودك حالة غير متوقعة. وعندما تُنفّذ تلك الحالة غير المتوقعة تحصل على استثناء لشيء كان متوقعًا.
رأينا بالفعل مجموعة من أمثلة الاستثناءات المختلفة. تحدثنا حتى عن اثنين منها في المحاضرة الماضية. لدينا أخطاء الفهرسة (Index Errors)، حين تفهرس أبعد مما ينبغي داخل قائمة، وأخطاء النوع (Type Errors)، حين تفعل أشياء غريبة بالأنواع لا تحبها Python. وأخطاء الصياغة (Syntax Errors) استثناءات أيضًا. وأخطاء الأسماء (Name Errors) استثناءات أيضًا. إذًا مجموعة من هذه الأخطاء التي واجهناها أنواع من الاستثناءات.
اتضح في تجربتنا البرمجية حتى الآن أنه كلما حصلنا على استثناء يتعطل البرنامج فورًا. ولا نملك حقًا أي طريقة للتعامل مع هذه الاستثناءات. نقبل فقط حقيقة أنه تعطل ونعود إلى لوحة التنقيح. لكن يتبين أننا في Python نستطيع فعليًا كتابة كود للتعامل مع هذه الاستثناءات.
إذا حدث أن أطلق كودك استثناءً، أي وقع خطأ أو حدث شيء غير متوقع، تستطيع كتابة كود يتعامل مع ذلك الموقف. ويقرر إما تجاهل حقيقة وقوع خطأ، أو تعيين بعض القيم الافتراضية، أو إطلاق استثنائك الخاص فقط والمتابعة. سنرى مجموعة من أمثلة هذه الحالات.
الطريقة التي نتعامل بها مع الاستثناءات هي استخدام بعض كتل الكود (Code Blocks). نعالج الاستثناءات باستخدام كتلتي try وexcept. طريقة كتابة معالج استثناء (Exception Handler) هي وضع بعض الكود الذي قد يسبب مشكلة داخل كتلة try هذه. وtry كلمة مفتاحية (Keyword) في Python، لذلك لا يمكنك بالطبع تسمية متغير try أو شيء كهذا.
صفحة المصدر 2
إذا كتبتها في الكود فستراها تصبح زرقاء. وتخبر try لغة Python بأنك تبدأ كتلة كود تحتوي على بعض الأسطر التي تريد من Python تنفيذها. أي كود عادي فقط. إذا استطاعت Python تنفيذ هذه الأسطر بنجاح دون إطلاق استثناء، أي دون تعطل البرنامج، فلا يحدث شيء. لا يُشغّل شيء داخل كتلة except هذه.
ويستمر الكود كالمعتاد. لكن إذا حدث أن شيئًا غريبًا وقع في ذلك الكود الذي شغّلته، وكان الكود سيتعطل، فإنه لا يتعطل فعليًا، لأننا نستطيع التقاط الاستثناء (Catch) الذي يُطلق داخل كتلة except هذه. فإذا كانت لدينا كتلة except هنا مرتبطة بكتلة try هنا.
ستحاول Python تشغيل هذا الكود الذي قد يسبب مشكلة. وإذا أُطلق استثناء، ستتوقف عن تشغيل أي أسطر أخرى داخل كتلة try وتنتقل فورًا إلى الأسطر داخل كتلة except. ثم تُنفّذ الأسطر داخل كتلة except. ولرسم تشابه فقط، لو أردت قول هذا باستخدام if وelse.
الطريقة التي سأصف بها كتلتي try وexcept هي أن أقول if، ثم أضع جميع أسطر الكود التي قد تسبب مشكلة والتي أريد كتابتها داخل هذا الشرط لـif. وإذا استطاعت جميع هذه الأسطر العمل بنجاح فلا يحدث شيء آخر. داخل if هذه هو أساسًا pass فقط. ولا ننفّذ else، ثم نواصل بقية حياتنا.
لكن إذا كان هناك سطر من الكود هنا نحاول تشغيله ويتعطل، أو يسبب تعطل البرنامج، ستقول Python: لا، لن أتعطل بعد، دعني أرى ما الذي يريد الكود مني فعله. فننتقل إلى داخل else، ثم نفعل شيئًا لمعالجة المشكلة. ذلك الشيء الذي نفعله يوجد داخل كتلة except هذه.
مرة أخرى، هذا ليس كودًا سنكتبه أبدًا. إنه مجرد طريقة نوعًا ما لرسم تشابه مع ما نعرفه حتى الآن. الكود الذي سنكتبه هو try ثم مجموعة من الكود، ثم except وتنفيذ بعض الأسطر إذا ظهر خطأ في كتلة try. لننظر إلى بعض الأمثلة بكود ينبغي أن تستطيعوا كتابته في هذه المرحلة من المقرر.
لدينا بعض الكود هنا على اليسار. إنه دالة اسمها sum_digits. ونكتب هذا الكود دون أي استثناءات، حسنًا؟ نكتبه فقط كما لو أعطيتم هذا الكود في اختبار. ما أحد الحلول؟ تأخذ sum_digits هذه سلسلة نصية S. ونقول إنها غير فارغة وتحتوي على بعض الأرقام، وأريد إرجاع مجموع كل المحارف التي هي أرقام.
لا أقول فعليًا أي شيء عما إذا كانت السلسلة S تحتوي على محارف غير رقمية. لكن لنكتبها بطريقة متينة (Robust) على أي حال. ستكون لدينا حلقة تمرّ على كل محرف في السلسلة S. وأستخدم هنا الكلمة المفتاحية in، هذه الحيلة الصغيرة اللطيفة التي تقول: إذا كان ذلك المحرف، أيًّا كان، موجودًا داخل سلسلة الأرقام هذه، فأعرف أنه عدد — آسفة، أعرف أنه رقم. وسأحوّل ذلك الرقم، من 0 إلى 9 أيًّا كان، إلى عدد صحيح، وأضيفه إلى مجموعي التراكمي. ثم تفعل تلك الحلقة عملها إلى أن تنتهي. ثم أُرجع المجموع. من حيث تشغيل الكود، هذا كما هو على الشريحة. إذا أعطاني المستخدم هنا السلسلة 1, 2, 3، فسأجمع 1 زائد 2 زائد 3، والنتيجة ستة. ممتاز.
صفحة المصدر 3
وإذا أعطاني المستخدم 1, 2, 3، ثم بعض المحارف العشوائية التي أعرف أنني لا أستطيع جمعها، فستظل Python قادرة على تقييمه، لأن تعليمة if هذه لن تُنفّذ، صحيح؟ بالنسبة إلى A وB وC لا ندخل داخل if. لذلك لا حاجة إلى تحويل أي شيء. حسنًا. ما يزال الكود يعمل.
لو لم تكن لدي if هنا، لو قررت فقط تحويل كل شيء يأتيني إلى int، فسيظل السطر الأول من الكود يعمل، لكن السطر الثاني سيطلق استثناءً. هل ترون؟ لدي على الجانب الأيمن النص الأحمر المخيف الذي يقول ValueError: invalid literal for int() with base 10: 'a'. يصعب فهمه قليلًا.
لكن بعدما ترون مجموعة من هذه الرسائل ستعرفون أن هناك شيئًا يتعلق بالأنواع عندي. وأنني أحاول تحويل — أحاول تحويل السلسلة النصية a إلى عدد صحيح. بالطبع لا تعرف كيف تفعل ذلك. لذلك هذا هو السطر الذي قد يسبب المشكلة، صحيح؟ التحويل إلى عدد صحيح.
لنحاول كتابة الكود نفسه، لكننا سنفعل ذلك الآن بمعالجة الاستثناءات (Exception Handling). سيكون جزء كبير منه كما هو. ما سنغيّره هو — الكود الذي قد يسبب مشكلة هو هذان السطران هنا، صحيح؟ لم أعد أحتاج إلى if.
بدلًا منها سأفترض فقط أنني أستطيع تحويل كل محرف إلى عدد صحيح. وسأحاول فعل ذلك. سأحاول تحويل كل محرف إلى عدد صحيح، ثم إضافته إلى مجموعي التراكمي. سيعمل ذلك معظم الوقت إذا كان المدخل رقمًا، لكن المستخدمين يعطونني أحيانًا شيئًا غير رقمي.
في تلك الحالة رأيتم ما يحدث: يطلق الكود ValueError. فإذا لم تكن لدينا كتلة except ولا كتلة try، يتعطل الكود فورًا. لا تُعطى حتى إجابة. لكن مع كتلة except ستقول Python: آه، في هذه الجولة المحددة من حلقة for أُطلق استثناء. لذلك سأنفّذ ما يوجد داخل كتلة except.
وتقول كتلة except: اطبع «لا يمكن تحويل»، ثم المحرف الذي لم تستطع تحويله في تلك المرة عبر الحلقة. ثم ينتهي تكرار الحلقة ذاك وينتقل إلى المحرف التالي في التسلسل. لنشغّل الكود. وهذه هي sum_digits مع except.
هنا لدي مستخدم يعطيني مدخلًا سليمًا تمامًا. لا تُطلق استثناءات. عمل الكود جيدًا. إذا أعطاني المستخدم بعض المحارف داخلها، تمرّ الحلقات. تجمع 1 زائد 2 زائد 3. لكن عندما تحاول تحويل a إلى عدد صحيح، هنا، عندما يصل التكرار إلى a، ستقول إن هذا يطلق ValueError، كما رأينا للتو.
وسأنفّذ أي شيء داخل كتلة except. فتطبع «تعذر تحويل المحرف». ها هو. ثم أعطيت المستخدم فعلًا المحرف الذي تعذر تحويله. تنتقل إلى التكرار التالي، المحرف التالي في التسلسل. B؛ مرة أخرى تحاول تحويل B. لا تستطيع تحويله إلى عدد صحيح. فنطبع «تعذر تحويل B».
وأخيرًا C، الأمر نفسه مع C. هل هذا منطقي؟ هل هذا واضح حتى الآن؟ إذًا لدينا نوعًا ما موقف صغير يشبه if وelse هنا؟ جميل. من المواضع المناسبة لوضع كتل try/except التعامل مع مدخلات المستخدم، لأن المستخدمين، عندما يعطونك مدخلات باستخدام أمر input الفعلي، يصعب توقعهم جدًا.
لا نعرف أنواع الأشياء التي سيعطونك إياها، حتى إن أعطيتهم تعليمات واضحة بأن يخبروني بعدد أو بعدد آخر. في هذه الأسطر الثلاثة من الكود هنا في الأسفل لدي المستخدم يعطيني عددين. ثم أطبع الأول مقسومًا على الثاني.
صفحة المصدر 4
أنا مستخدمة لطيفة. أفعل ما يُطلب مني، لذلك سأُجري 5 مقسومة على 8. ممتاز. يعمل الكود جيدًا. لنفترض أن شخصًا آخر يشغّل الكود، وهذه المرة يقرر إجراء seven مقسومة على، لا أعرف، five، هكذا. ValueError. هذا إذًا أحد الأشياء التي قد تسوء. يحاول المستخدم أن يكون ظريفًا.
وشيء آخر قد يسوء هو، لنفترض أن المستخدم يعطيني 0 بوصفه العدد الثاني. في هذه الحالة أحصل على ZeroDivisionError. تستطيعون رؤية أن Python لا تعرف كيف تقسم على 0. لذلك تطلق استثناءً. هذا الشيء هو ZeroDivisionError.
هذه حالة أتعامل فيها مع مدخلات قد تسبب مشكلة. سأغلّف إذًا أسطر الكود التي قد تسبب مشكلة داخل كتلتي try وexcept. لدي تلك الأسطر الثلاثة التي سأحاول تنفيذها. وإذا حدث أي خطأ، سأنفّذ ما يوجد في كتلة except، وكل ما أفعله هو طباعة «خطأ في مدخلات المستخدم».
حسنًا. لنشغّل ذلك. هذا هو هنا. هنا، مرة أخرى، تعمل المدخلات الصحيحة جيدًا. وإذا أعطاني المستخدم حرفًا، «خطأ في مدخلات المستخدم». هذه طريقة ألطف وأكثر ودًّا بكثير لتعطل البرنامج من ValueError أو أيًّا كانت الرسالة. ومرة أخرى إذا أعطاني المستخدم 0، «خطأ في مدخلات المستخدم». مرة أخرى، طريقة ألطف بكثير لتعطل البرنامج.
ما نستطيع فعله فعليًا هو أن تكون لدينا سلوكيات محددة بحسب الاستثناءات التي تُطلق، صحيح؟ ربما لا أريد نصًا عامًا يقول «خطأ في مدخلات المستخدم» لكلتا هاتين الحالتين، صحيح؟ ربما إذا قسم المستخدم على 0، أريد إعطاءه رسالة مختلفة عما إذا أعطاني حرفًا.
في تلك الحالة أستطيع أن تكون لدي كتل except مختلفة لكل خطأ مختلف قد يظهر. وأنا أكتب هذا الكود، أستطيع التفكير مسبقًا، صحيح؟ وأستطيع محاولة التقاط أخطاء محددة. هنا أستطيع التقاط ValueError. فأقول except ثم أقول اسم الخطأ الذي أريد التقاطه.
هذه الكتلة من الكود، كتلة except هذه، لن تُشغّل إلا إذا تعطل الكود في كتلة try بخطأ ValueError المحدد ذاك، صحيح؟ ثم أستطيع أيضًا التقاط ZeroDivisionError هنا في الأسفل. وفي هذه الحالة بالتحديد لا تُشغّل كتلة except هذه إلا عندما يُطلق ZeroDivisionError، صحيح؟
هنا في ValueError سأطبع للمستخدم «لم أستطع التحويل إلى عدد». رسالة خطأ أكثر تحديدًا كي يعرف ما يحدث. وفي ZeroDivisionError أستطيع أيضًا الإشارة إلى أن هناك مشكلة، لا أستطيع القسمة على 0، بطباعة ذلك على الشاشة.
ثم أستطيع القول إن a مقسومة على b هي اللانهاية (Infinity). وأستطيع متابعة التعليمة الأخيرة التي كان يُفترض تنفيذها في كتلة try، a+b؛ أستطيع إعطاءهم إجابة a+b لأننا نستطيع إضافة 0 إلى a. لا مشكلة. كتلة except الأخيرة هنا في الأسفل تلتقط أي شيء آخر ليس ValueError وليس ZeroDivisionError.
لا أستطيع التفكير في أي شيء قد يسوء. فإذا حدث أن دخلنا هنا، فقد حدث شيء خاطئ جدًا. سأقول إن الشيء الوحيد الذي أستطيع التفكير فيه هو أن تكون ذاكرة حاسوبنا على وشك النفاد، ويؤدي تشغيل هذه القطعة الصغيرة من الكود إلى تجاوز الحد، صحيح؟ عندئذ ستتعطل Python على الأرجح، وربما تطبع ذلك الخطأ قبل إيقاف الحاسوب بالكامل أو شيء كهذا.
لكن الأخيرة ينبغي ألا تعمل حقًا أبدًا. دعوني أريكم ما يحدث عندما نشغّل الكود بهذه الكتل المحددة من except الآن. إذا أعطاني المستخدم مدخلات لطيفة تمامًا، يتقدم البرنامج كالمعتاد. يُنفّذ كل سطر داخل كتلة try هنا. ولا تُنفّذ أي كتلة من كتل except.
ملاحظة المترجم: المقصود بـ
sevenوfiveفي المثال إدخالهما كلماتٍ إنجليزية بدل الأرقام؛ لذلك حُفظتا بالإنجليزية. وإسناد اللانهاية في مثال القسمة على الصفر قرار في هذا المثال، لا ناتج قسمة صحيحة تنفّذها Python.
صفحة المصدر 5
يعطيني المستخدم حرفًا. أنهي البرنامج بسلاسة برسالة «تعذر التحويل إلى عدد»، فيعرف عندئذ أنني أمسكت به. والأخيرة هي أن أحاول القسمة على 0. مرة أخرى لدي الرسالة الصغيرة «لا يمكن القسمة على 0». ثم أعطيهم ناتج قسمتهم بوصفه اللانهاية.
وa+b هي 6. أنفّذ إذًا كل أسطر الكود التي تُلتقط هنا. أسئلة حتى الآن؟ يبدو الأمر واضحًا حتى الآن. حسنًا. هذه طرق جميلة جدًا لنتعامل مع الاستثناءات التي تُطلق في برامجنا. والأشياء التي أخبرتكم بأننا نستطيع ربطها بكتلة try هي كتلة except، صحيح؟
فعلنا ذلك. لكننا نستطيع فعليًا ربط شيئين آخرين بكتل try. ولا نستخدمهما كثيرًا في هذا المقرر، لكنني فكرت فقط في ذكرهما. إذا كانت لديك كتلة else مرتبطة بكتلة try، فهذا يعني أن أسطر الكود داخل كتلة else تُنفّذ عندما يُنفّذ كل شيء داخل كتلة try بصورة مثالية.
إذا سار كل شيء وفق الخطة، فأي شيء تضعه داخل كتلة else سيُنفّذ أيضًا. ثم تستطيع أن تكون لديك كتلة finally كذلك. كما لدينا try وexcept، تستطيع أيضًا أن تربط بهما finally. وسيُنفّذ جسم finally مهما حدث.
إذا أُطلق استثناء، تنفّذ finally أيضًا. وإذا عمل الكود تمامًا دون إطلاق أي أخطاء، تُنفّذ finally أيضًا. أعطيت مثالًا هنا لشيء قد تضعه داخل كتلة finally، وهو نوع من كود التنظيف (Cleanup Code). إذا كتبت كودًا يفتح ملفات من نظام الملفات (File System)، فمن الجيد إغلاقها قبل إنهاء برنامجك.
هذا شيء قد تريد فعله داخل كتلة finally: إغلاق الملفات فقط قبل الإغلاق، قبل انتهاء برنامجك. حسنًا. أريتكم كيفية التعامل مع الاستثناءات، لكن ماذا نفعل الآن داخل كتل except؟ حسنًا.
فعلنا أمرين مختلفين، أغلبهما طباعة أن شيئًا قد سار على نحو خاطئ، لكننا نستطيع فعل أشياء أخرى متنوعة. أحد الأشياء، ولا أنصح بفعل هذا، هو الفشل بصمت (Fail Silently) فقط. يمكننا بالتأكيد كتابة كود يحتوي أساسًا على — نعم. هناك سؤال.
أحد الحضور (AUDIENCE):
آسف. في المرة الأخيرة عندما قلتِ —
آنا بيل (ANA BELL):
نعم.
أحد الحضور (AUDIENCE):
كيف تختلف عن else العادية؟
آنا بيل (ANA BELL):
هذه else نربطها بـtry. سنفعل شيئًا مثل else، ثم تطبع شيئًا هنا، «نجاح» أو شيء كهذا. ثم إذا نُفّذ الكود بصورة مثالية دون خطأ، ستطبع أيضًا «نجاح».
أحد الحضور (AUDIENCE):
حسنًا.
آنا بيل (ANA BELL):
نعم. فما الذي نفعله داخل كتل except؟ أحد الأشياء هو الفشل بصمت، وهذا يعني أنه يمكنك محاولة تنفيذ قطعة الكود كلها، ثم قول except. ثم يكون السطر الوحيد لديك في except ربما سطرًا يقول pass.
صفحة المصدر 6
هذا يعني أنك ستلتقط أي خطأ يحدث، لكنك لا تفعل شيئًا إطلاقًا وتدع البرنامج يستمر مع احتمال تمرير قيمة سيئة. هذه ليست فكرة جيدة. يمكنك أيضًا استبدال قيم تعرف أنها ربما تكون إشكالية بصمت دون الإشارة إلى ما يحدث. هذه أيضًا ليست أفكارًا جيدة.
على الأقل ينبغي أن تشير بشيء في المخرجات إلى أن شيئًا غريبًا قد حدث. شيء آخر تستطيع فعله هو إرجاع قيمة خطأ ما. يمكن أن تكون لديك مجموعة من المتغيرات في الكود تضبطها على قيم ما، أشبه بالأعلام (Flags)، كلما دخل كودك كتلة استثناء، صحيح؟
لكن المشكلة في ذلك هي أنك تحتاج الآن إلى التحقق من كل هذه القيم لاحقًا في الكود، صحيح؟ يصبح الكود الآن معقدًا أكثر من اللازم، لأن لديك مجموعة من المتغيرات الإضافية التي تتحقق منها باستمرار لمعرفة ما إذا أُشير إلى أي أخطاء أو حدث شيء كهذا. شيء أخير، وهذا ما سأريكم أنه يمكنكم فعله، هو إيقاف التنفيذ فعليًا رغم ذلك.
مثلما عندما أدخلنا، عندما حاولنا تشغيل برنامج sum_digits وتعطل بـValueError، يمكننا رغم ذلك جعل برنامجنا يتعطل، لكن بشروطنا نحن. نستطيع إطلاق أخطاء ValueError الخاصة بنا، أو أي نوع من الأخطاء تريد إطلاقه، مع رسالتك المخصصة. ما يزال الكود يتعطل، ولا بأس بذلك، لأنك ربما لا تريد أن يتابع الكود الإشكالي، لكنك تجعله أساسًا يتعطل برسالة مخصصة ونوع خطأ مخصص يُطلق.
هذه كلمة مفتاحية في Python. تطلق ValueError الخاص بك باستخدام raise، ثم تضع بين الأقواس أي رسالة تريد وضعها. ها هو مثال لـsum_digits نطلق فيه استثناءنا الخاص. لنفترض أننا بالفعل أردنا من المستخدم إعطاءنا أرقامًا فقط، ولا نريد فعليًا أن تستمر هذه الدالة في العمل وتمرير المجموع إذا أعطانا المستخدم في أي وقت سلسلة تحتوي على حروف، صحيح؟
في تلك الحالة بالتحديد سأضع أيضًا كتلة try وكتلة except: كتلة try حول الكود الإشكالي، وكتلة except لالتقاط أي أخطاء. لكن الآن، بدلًا من طباعة شيء وترك الكود يتابع الحلقة، سنطلق ValueError برسالتنا الخاصة. رسالتي هي أن هذه السلسلة احتوت على محرف.
إذا شغّلت هذا الكود، وهو في الحقيقة هنا في الأعلى، إذا شغّلته بمدخلات سليمة تمامًا، فلا مشكلة، صحيح؟ نحسب المجموع كما نريد فقط. لكن إذا أعطانا المستخدم سلسلة تحتوي على محارف إضافية، وربما لا نريد فعلًا حدوث ذلك، ترون أنه ما يزال لدي ValueError، وهو نوع الاستثناء نفسه الذي أُطلق دون try وexcept.
لكن الرسالة التي مررتها الآن هي «احتوت السلسلة على محرف»، بدلًا من invalid literal وأيًّا كانت تلك الرسالة المبهمة. هذه طريقة ألطف بكثير للإشارة إلى المشكلة أو لإيقاف البرنامج، لإنهائه، لكن بشروطك الخاصة. لنترككم تعملون على هذا لدقيقتين.
ستطلقون أخطاء ValueError الخاصة بكم. أريد منكم كتابة هذه الدالة المسماة «القسمة الزوجية (Pairwise Division)». تأخذ قائمتين. ينبغي أن تكون القائمتان غير فارغتين ومتساويتين في الطول، صحيح؟ وفقًا لهذا المثال، ها هما قائمتان غير فارغتين ولهما الطول نفسه. وأريد أن يمرّ الكود أساسًا عنصرًا بعنصر.
وينشئ قائمة جديدة يكون فيها كل عنصر، مثلًا، 4 مقسومة على 1، و5 مقسومة على 2، و6 مقسومة على 3. تنفّذ القسمة زوجيًا، وتضع جميع تلك العناصر في قائمة جديدة وتُرجع تلك القائمة. إذا كان المقام — أي المعامل الثاني الممرر Ldenom — يحتوي على أي 0، فأطلق ValueError. حسنًا.
ملاحظة المترجم: الاسم البرمجي لهذه الدالة في ملف الكود الرسمي للمحاضرة هو
pairwise_div؛ نُقل الاسم المنطوق في الفقرة بوصفه عبارة لا معرّف Python مخمّنًا.
صفحة المصدر 7
لا تدعوا الكود يتعطل بـZeroDivisionError، بل أطلقوا ValueError برسالة لطيفة. ابدؤوا بالكود الذي يؤدي المهمة فقط، ثم أضيفوا جزء ValueError في النهاية. حسنًا. هل لدى أحد بداية؟ كيف تريدون حل هذه المسألة؟ كيف تريدون كتابتها؟ نعم.
أحد الحضور (AUDIENCE):
[غير مسموع]
آنا بيل (ANA BELL):
نعم.
أحد الحضور (AUDIENCE):
ثم نحاول إجراء [غير مسموع]، L [غير مسموع] Lnum[i] على Ldenom[i] [غير مسموع]. آسف، لكل i، أظن، عبر كامل [غير مسموع].
آنا بيل (ANA BELL):
Lnum عند i مقسومة على Ldenom — عفوًا — عند i. ننفّذ القسمة لكل i في. هنا، ما i؟ هل هي العنصر أم فهرس؟ نعم. كيف نحصل أساسًا على الأعداد من 0 حتى طول إحدى هاتين القائمتين؟ إذا أردتم فعل ذلك. نعم.
أحد الحضور (AUDIENCE):
آه. إذًا for i in len(Lnum).
آنا بيل (ANA BELL):
نعم. علينا استخدام range، تذكروا. نعم، range(len(Lnum)). نعم. أظن أن هذه هي أقواسي. من الرائع أنك استخدمت استيعاب القوائم (List Comprehension) مباشرة. هل يريد أحد إعادة كتابة هذا باستخدام حلقة؟ صحيح. آه، نعم. تفضل.
أحد الحضور (AUDIENCE):
For e in Lnum.
آنا بيل (ANA BELL):
نعم.
أحد الحضور (AUDIENCE):
Lnum عند e — أو عليّ أن أقول كيف L تساوي L.append في [غير مسموع].
آنا بيل (ANA BELL):
ما زلنا نريد استخدام الفهرس، صحيح؟ لأننا إذا نظرنا إلى العنصر في Lnum، فسيصعب علينا الحصول على العنصر نفسه في Lnum. فلنمرّ على 0 عبر النطاق، صحيح؟ أساسًا ما فعلناه في الأعلى: range وlen، ثم اختر واحدة منهما لأنهما بالطول نفسه.
لنغيّر هذا الآن إلى i فقط حتى لا نرتبك. سأقول إن i هي 0, 1, 2, 3, 4، صحيح؟ نعرف الآن أن هذا هو الفهرس. ومع هذا الفهرس في أيدينا، هذه هي البداية الصحيحة، صحيح؟ تعطيني Lnum عند i العنصر في Lnum. أقسمه على Ldenom عند i نفسها تمامًا.
أحد الحضور (AUDIENCE):
ثم نأخذ L تلك عند [غير مسموع].
آنا بيل (ANA BELL):
نعم. L. نستطيع تنفيذ L.append، شيء كهذا. لا نستطيع القول إن L عند i تساوي ذلك، لأن L عندنا —
أحد الحضور (AUDIENCE):
ليست [غير مسموع].
آنا بيل (ANA BELL):
— ليست [غير مسموع]. بالضبط. نعم. ممتاز. يمكننا فعل هذا. هذه مجرد طريقة أخرى.
ثم في النهاية نستطيع إرجاع متغيرنا، صحيح؟ حسنًا. هذا يحل مسألتنا. كيف نضيف الجزء الذي نطلق فيه ValueError؟ كيف تريدون التحقق من أن Ldenom فيها 0؟ لأن هذا ينبغي، كما نأمل، أن يعمل مع كودنا دون — آه. عفوًا. هل فعلته مرتين؟ آسفة. نعم فعلت. دعوني أجعل واحدة منهما تعليقًا فقط. عفوًا. ها هي. كيف أضيف الجزء المتعلق بأخطاء ValueError؟ نعم.
ملاحظة المترجم: تقول المتحدثة
Lnumمرتين في تفسير صعوبة مقابلة العناصر؛ حُفظ الاسم كما في النص الرسمي بدل تبديله صامتًا إلىLdenom.
صفحة المصدر 8
أحد الحضور (AUDIENCE):
ضعها في try عندك. ثم تضيف except. ثم [غير مسموع] لا تحتوي على صفر.
آنا بيل (ANA BELL):
نعم. نستطيع وضع هذا داخل try ثم except وraise ValueError. نعم. ومع رسالة لطيفة هنا. رسالة لطيفة. ونستطيع أيضًا وضع حلقة for كلها تحت try. الكود ليس طويلًا جدًا. لن يُحدث ذلك فرقًا.
إذا حاولنا تشغيله هكذا الآن، فلدي ValueError مع رسالتي اللطيفة. نعم. طريقة أخرى نستطيع بها إطلاق ValueError، فقط لاستكمال الحديث، هي قول شيء مثل — يمكنك حتى إطلاق استثناءات داخل تعليمات if. ليس من الضروري أن تكون جزءًا من كتل except. قبل أن نفعل أي شيء بالكود، نستطيع القول: إذا كان 0 موجودًا في Ldenom، فأطلق ValueError.
سيكون ذلك أيضًا أمرًا جيدًا. حسنًا. نستطيع إذًا إطلاق أخطاء ValueError حيث نريد. أريد الآن الحديث قليلًا عن التوكيدات. التوكيدات ما تزال فعليًا استثناءات تُطلق. إنها فقط نوع خاص جدًا من الاستثناءات نستخدمه غالبًا أداةً للبرمجة الدفاعية (Defensive Programming).
تُستخدم التوكيدات أساسًا لفرض هذه العقود (Contracts) التي نضعها بين شخص يكتب دالة وشخص يستخدم دالة. وهي أساسًا سلاسل توثيق الدالة (Docstrings)، صحيح؟ عندما تحدثنا عن سلاسل التوثيق قلت إنه من الصعب جدًا علينا فرض النص داخل سلسلة التوثيق، صحيح؟
لأن الشخص الذي يكتب الدالة يقول إن قائمة المدخلات ينبغي ألا تكون فارغة، أو إن قائمتي المدخلات، كما في المثال السابق، ينبغي أن تكونا بالطول نفسه. ولا توجد طريقة فعلية لفرض ذلك. إنه مجرد شيء من الجميل وجوده. وسنضمن عمل الكود إذا روعيت هذه الأشياء.
لكن يتبين أن التوكيدات طريقة جميلة فعليًا لكي نضيف، شيء جميل نضيفه إلى دوالنا، بحيث تحاول فرض هذا العقد من خلال المواصفات. طريقة إنشاء توكيد هي أن نقول assert، وأنا أؤكد أن هذه العبارة صحيحة. إذا أردت أن يكون طول المدخل لدالة غير صفري، فسأؤكد أن طول L لا يساوي 0 أو شيء كهذا.
إذا كان التوكيد صحيحًا، إذا تحقق ذلك الشرط، يتابع الكود كالمعتاد. لكن إذا لم يكن التوكيد صحيحًا، تنتهي Python بـAssertionError، ثم رسالة ما بأن الشرط لم يكن صحيحًا. وهذه جميلة جدًا، لأنها توقف تنفيذ البرنامج بمجرد عدم الالتزام بذلك العقد، صحيح؟ بمجرد أن يحدث شيء خاطئ داخل المواصفات، ينتهي البرنامج بأخطاء التوكيد تلك.
ومن الجيد رؤيتها؛ لأنك إذا كنت تنقّح الكود، لا تريد تمرير قيم سيئة عبر الدوال، لأن تلك القيمة قد تنتقل إلى مواضع أبعد وأبعد وأبعد، ثم تجعل تنقيحك صعبًا جدًا. حسنًا؟ إذا أوقفت التنفيذ بمجرد أن يكون شيء غريبًا أو غير مضبوط، مثل عدم تحقق افتراض يتعلق بمعامل، فهذا جيد.
في مثال sum_digits، ها هو الكود الذي كتبناه أخيرًا. من total حتى النهاية هو بالضبط ما كان لدينا من قبل. كل ما سنضيفه هو تعليمة assert هذه في الأعلى، بأن الطول ليس فارغًا، لأن جزءًا من عقدي هنا هو أن s سلسلة نصية غير فارغة، صحيح؟ هذا شيء جميل نؤكده.
إذا أعطانا المستخدم في أي وقت سلسلة فارغة، سينتهي البرنامج. في هذا المثال هنا لدي sum_digits مع assert. إذا أعطانا المستخدم سلسلة فارغة، لا تُنشأ total، ويكون التوكيد كاذبًا فورًا. فالطول كان يساوي 0. أُطلق AssertionError برسالة «s فارغة». أي ما كان لدي هنا.
صفحة المصدر 9
إذا كان المدخل سليمًا، فلا يُطلق توكيد، ويتابع الكود كالمعتاد. هذا جميل. لنترككم تضيفون سطرًا آخر من الكود إلى البرنامج الذي كتبناه للتو. أضيفوا فقط تعليمة assert تفرض العقد. لدي Lnum وLdenom قائمتان غير فارغتين ومتساويتان في الطول.
يمكنكم تنفيذ هذا كله في تعليمة assert واحدة، أو وضع تعليمتي assert منفصلتين مع رسالتين منفصلتين. أي طريقة تريدونها تناسبني. سأعطيكم دقيقة للعمل عليه، ثم نستطيع كتابته. حسنًا. ما التوكيدات التي ينبغي أن أضعها هنا؟ نعم.
أحد الحضور (AUDIENCE):
ربما assert len(Lnum) ثم [غير مسموع] Ldenom، ثم [غير مسموع].
آنا بيل (ANA BELL):
الشيء الذي أؤكده ينبغي أن يكون صحيحًا. هل أريدهما أن يكونا متساويين؟ نعم. بالضبط. أريد len(Lnum) أن تساوي len(Ldenom). نعم. Ldenom، هكذا. نعم، هذه واحدة، صحيح؟ الشيء الذي تريده تؤكد أنه صحيح، وإلا، فاصلة، سنضع رسالة، صحيح؟
«الأطوال مختلفة» أو شيء كهذا. هل تريد تنفيذ تعليمة assert الأخرى؟ أم يريد شخص آخر محاولة التوكيد الآخر؟ الآخر هو أنهما قائمتان غير فارغتين. نعم.
أحد الحضور (AUDIENCE):
وassert أن طول Lnum ليس، مثل علامة تعجب. والأمر نفسه لـLdenom. وهذا ينبغي أن يزيل [غير مسموع].
آنا بيل (ANA BELL):
نعم. نستطيع بالتأكيد فعل ذلك. لا يساوي 0، فاصلة. «قائمة فارغة» أو شيء كهذا. نعم. جميل جدًا. نحاول هنا فرض عقودنا اللطيفة. ولدي مثالان هنا في الأسفل.
هنا لدي قائمتان مختلفتان في الطول. ها هو. أُطلق توكيدي برسالة لطيفة، «الأطوال تختلف». ثم يتوقف الكود فورًا، ويجبرنا على التحقق لمعرفة لماذا تختلف هذه الأطوال. لذلك لن تنتقل هذه القوائم السيئة إلى مواضع أبعد إذا كانت لدي أجزاء أكبر من الكود.
والأمر نفسه هنا. لدي AssertionError بأن عندي قائمة فارغة. أي أسئلة حتى الآن؟ حسنًا. هناك مثال آخر أريد المرور عليه. لن أشغّل هذا فعليًا، لكنه موجود في شرائح Python. أردت فقط إعطاءكم مثالًا آخر لكيفية استخدام الاستثناءات والتوكيدات في سياق مختلف.
وآمل أن يُظهر أنه بصفتك مبرمجًا تستطيع اختيار طريقة إضافة هذه الاستثناءات والتوكيدات، صحيح؟ في أي موضع يبدو من المعقول إضافتها، ينبغي أن تضيفها. في هذا المثال بالتحديد نفترض أن لدينا قائمة صف دراسي. في هذه الحالة لدي طالبان فقط في صفي.
هذه درجاتهما في الاختبارات. لدي قائمة تبدو هكذا. تبدو معقدة، لكنني سأشرحها لكم. هذا طالب واحد في قائمتي. وهذا طالب آخر في قائمتي. لدي إذًا قائمة من قوائم، حيث هذه الأشياء التي ظللتها بالأحمر هي طلابي. ولكل طالب لدي قوائم أكثر ضمن معلوماته.
القائمة الأولى المتعلقة بطالب واحد هي اسمه، صحيح؟ العنصر الأول اسمه الأول. والعنصر الثاني اسم عائلته. ثم القائمة الثانية لذلك الطالب هي درجاته في المقرر. مجرد قائمة أخرى بجميع درجاته في المقرر. حسنًا. ما أريد فعله، وهذا هو الكود الذي سأمرّ عليه، هو أنني أريد إنشاء قائمة جديدة بناءً على درجات اختبارات GRE الأصلية، تحتوي على المعلومات نفسها كما من قبل.
تستطيعون رؤية أنه ما يزال لدي قائمتان للطلاب، الصف الأول والصف الثاني. وفي معلومات كل طالب ما يزال لدي أيضًا اسمه وقائمة درجاته، لكنني أضيف عنصرًا آخر في النهاية لكل طالب، وهو متوسط (Average) قائمة الدرجات. أخذت متوسط هذه ووضعتُه بوصفه عددي الصحيح أو العشري في النهاية.
ملاحظة المترجم: تظهر
GREفي النص الإنجليزي الرسمي في عبارة درجات الاختبارات الأصلية؛ أُبقيت كما وردت، مع أن سياق المثال يتحدث عن درجات المقرر ولا يشرح هذا الاختصار.
صفحة المصدر 10
والأمر نفسه مع طالبي التالي. الكود الذي سيفعل هذا يبدو هكذا. هذه مجرد القائمة الأصلية لتعطيكم مثالًا تنظرون إليه، لأنني أجد رؤية الأشياء صعبة دون أمثلة. هذا هو الكود الذي يحصل على إحصاءات الصف الدراسي. ينشئ القائمة الجديدة التي تحتوي على المتوسط عندي في النهاية لكل طالب في قائمة الصف.
مثلًا، الطالب هنا، stu، سيكون هذه القائمة التي تضم قائمتين. ما سأفعله في القائمة الجديدة التي أنشئها هنا، new_stats، هو إلحاق الطالب عند 0، وهو اسمه فقط. مجرد نسخ ولصق مباشر. والطالب عند 1، وهي درجاته فقط. مرة أخرى، نسخ ولصق مباشر لقائمة درجاته كاملة.
ثم سأطبق دالة اسمها average على قائمة درجات الطالب. وما سنراه في الشريحتين القادمتين هو أننا سنرى بعض التنويعات على دالة average هذه وما يحدث عندما نطبق هذه الدوال المختلفة. لكن الآن سأفترض أن هذا الكود سيؤدي المهمة. دالة average الأصلية ستأخذ قائمة من الدرجات.
ستبدو grades هذه مثل هذا الصندوق الأزرق هنا، صحيح؟ مجرد قائمة من الأعداد. ستجمع كل الدرجات، أي مجموع كل العناصر داخل تلك القائمة، وتقسمه على عددها: المتوسط. حسنًا. لنفترض الآن أن لدي طالبًا لا يحتوي على أي درجات اختبارات قصيرة أو أي درجات إطلاقًا.
في تلك الحالة تكون قائمة درجاته فارغة. إذا حاولت تطبيق مجموع الدرجات كلها مقسومًا على طول الدرجات لشخص لا توجد لديه معلومات درجات، فسيكون ذلك الطول 0. سأحصل إذًا على ZeroDivisionError عندما أشغّل الكود، وسيتعطل.
ما أريد فعله هو تغيير دالة average لمحاولة التقاط خطأ كهذا. سأحاول إجراء المجموع مقسومًا على الطول. وسألتقط ZeroDivisionError هذه داخل كتلة except. وكل ما سأفعله هو طباعة «تحذير: لا توجد بيانات درجات». لأي طالب توجد لديه فعلًا معلومات درجات هنا، سيعمل الكود — سيعمل كود الحصول على المتوسط تمامًا، لأنه سينفّذ المجموع مقسومًا على الطول.
هذا يعني أن كتلة try ستنجح، وسنُرجع المجموع مقسومًا على الطول. لكن إذا دخل أي طالب خطأ القسمة على الصفر هنا، فسنطبع شيئًا. وماذا نُرجع؟ ما الذي تُرجعه الدالة إذا دخلنا كتلة except؟
هذا هو الشيء الذي سيُطبع، لكن ما الذي تُرجعه هذه الدالة فعلًا؟ أي قيمة؟ None. بالضبط، صحيح؟ لا يوجد return داخل كتلة except هذه. ولا كود بعدها أيضًا. تستطيعون رؤية أنه إذا اكتملت بنجاح لهؤلاء الطلاب الثلاثة، تكون لدينا أعدادنا. هذا ما أرجعناه. لكن بالنسبة إلى الطالب الأخير الذي لا توجد لديه معلومات درجات، نُرجع None.
حسنًا. لا يعجبني ذلك. ما أريده في سجل درجاتي هو أن تكون لدي أعداد بوصفها قيمتي هناك. بدلًا من ذلك، لنُضف return إلى كتلة except تلك. ما زلنا سنشير إلى الخطأ. ما زلنا نريد معرفة أن شيئًا غريبًا قد حدث. لا أريد فقط إرجاع 0 دون إخبار المستخدم فعلًا بأن شيئًا حدث.
ما زلت أشير إلى الخطأ، لكنني أستطيع بعد ذلك إرجاع 0، لتظل القيمة عددًا. وعندئذ إذا فعلت شيئًا بالأعداد في النهاية يعمل كل شيء. كان هذا اختبارًا أول صعبًا بشكل خاص: 10, 10, 80.
حسنًا. شيء أخير نستطيع فعله هو مجرد التوكيد، صحيح؟ إذا أردنا التأكد من أن كل طالب لديه نوع من معلومات الدرجات، ربما إذا كانت بيانات الدرجات فارغة يكون شيء غريب قد حدث في دالة سابقة ربما استُدعيت، لا أعرف، لكن ربما نقول: لنؤكد فقط أن طول الدرجات ليس 0، فنحن لا نريد تنفيذ هذا الكود إلا إذا كانت هناك معلومات درجات.
صفحة المصدر 11
وإن لم تكن، فلنطلق AssertionError فقط للاحتياط. نستطيع تأكيد أن طول grades لا يساوي 0. في تلك الحالة سينتهي الكود بمجرد أن نحاول الحصول على معلومات الطالب الأخير. سيتعطل، وسيتعطل مع تعليمة assert هذه بأنه لا توجد بيانات درجات.
ثم نستطيع العودة إلى الكود والنظر: هل كنا نتوقع فعلًا أن تكون لدى الطالب معلومات أم لا؟ ثم نستطيع محاولة معالجة الأمر. إذًا هذا مجرد ملخص سريع للاستثناءات والتوكيدات. آمل أن تكون هذه المحاضرة قد أزالت نوعًا ما الغموض عن بعض هذه الاستثناءات التي كنا نحصل عليها.
أظهرت لكم أنها ليست مخيفة كما ربما بدت في البداية. لكنها لا يجب أن تنهي البرنامج دائمًا، صحيح؟ تستطيعون التقاطها. تستطيعون التعامل معها بأي طريقة منطقية لتلك الدالة أو لذلك البرنامج بالتحديد. تستطيعون طباعة مخرج لطيف على الشاشة.
تستطيعون تعيين بعض القيم الافتراضية. وتستطيعون أيضًا إنهاء البرنامج، لكن بشروطكم الخاصة، بأخطائكم الخاصة، وبرسائلكم المخصصة الخاصة، حتى يرى المستخدمون شيئًا ألطف من رسائل Python الافتراضية، صحيح؟ لذلك فإن معالجة الاستثناءات جزء مهم جدًا من كتابة برنامج، خصوصًا إذا كنتم تتوقعون حدوث أشياء غريبة، صحيح؟
أما التوكيدات فهي نوع من الاستثناءات. وهي مفيدة، كما ذكرت، لمحاولة فرض هذه العقود، هذه المواصفات. تستخدم التوكيدات أساسًا لأنك لا تريد انتقال قيم سيئة إلى مواضع لاحقة. لذلك بمجرد حدوث شيء ليس كما تتوقعه، يُطلق AssertionError وينتهي البرنامج فورًا، مما يسمح لك بالتحقق لمعرفة لماذا بالضبط لم تتحقق تلك الشروط.
حسنًا. هذا كل شيء. هذا كل ما كان لدي.