الأخطاء مجرد قيم بمزاج متعكّر
حين يحدث خطأ ما أثناء تشغيل برنامج بايثون — كالقسمة على صفر، أو محاولة قراءة ملف غير موجود، أو تحويل نص لا يمثّل رقمًا — يقوم بايثون بإنشاء كائن استثناء (exception) ويبدأ بالرجوع عبر مكدّس الاستدعاءات بحثًا عمّن يلتقط هذا الاستثناء. وإذا لم يلتقطه أحد، ينتهي تنفيذ البرنامج ويطبع تتبّعًا (traceback) للخطأ.
الاستثناءات في بايثون ليست شيئًا سيئًا بحدّ ذاتها، بل هي الطريقة التي يقول بها بايثون: "لا أستطيع إكمال هذه العملية، وهذا هو السبب." ومهمّتك أنت هي أن تقرّر، حالةً بحالة، أيّها تعرف كيف تتعامل معه، وأيّها يجب أن تتركه يمرّ إلى الأعلى.
الشكل الأساسي لـ try except في بايثون
try:يفتح الكتلة التي تحتوي على الكود المحتمل أن يرمي خطأً.except ValueError:يلتقط هذا الاستثناء تحديدًا إذا حدث داخلtry.- إذا لم يحدث أي استثناء، يتم تجاوز
exceptبالكامل.
جرّب تشغيل الكود بالقيمة 42 — يعمل بشكل سليم. جرّبه بالقيمة hello — سيتم تنفيذ كتلة المعالجة.
التقاط استثناءات محددة في بايثون
تمتلك بايثون شجرة هرمية من أنواع الاستثناءات. وهذه بعض الأنواع التي ستصادفها كثيرًا:
ValueError— القيمة المُمرَّرة غير صالحة بطريقة ما (مثلint("abc")أو وسيط خارج النطاق المسموح).TypeError— استُخدم نوع خاطئ (مثل"hi" + 3).KeyError— مفتاح غير موجود في القاموس.IndexError— فهرس خارج حدود التسلسل.FileNotFoundError— الملف غير موجود.ZeroDivisionError— محاولة القسمة على صفر.AttributeError— الكائن لا يملك الخاصية المطلوبة.
التقط الاستثناء المحدد الذي تعرف كيف تتعامل معه:
يمكنك التقاط أكثر من استثناء في جملة except واحدة عن طريق تمرير tuple:
لاحظ استخدام as e، فهو يربط كائن الاستثناء بالمتغير e حتى تتمكن من قراءة رسالته أو الاطلاع على خصائصه.
تجنّب التقاط كل شيء
عبارة except: المجرّدة تلتقط أي شيء حرفيًا، بما في ذلك KeyboardInterrupt (أي ضغطة Ctrl-C منك) والخروج على مستوى النظام. لا تستخدمها أبدًا.
أما except Exception: فهو أفضل قليلًا، لكنه يظل خطيرًا، إذ يبتلع أخطاءً برمجية لم تتوقعها ويُخفي المصدر الحقيقي للمشكلة:
# Don't do this without a really good reason.
try:
do_something()
except Exception:
pass
التصرف الصحيح في معظم الحالات هو التقاط الاستثناء المحدد الذي تعرف كيف تتعامل معه. وإذا وصل استثناء غير متوقع إلى أعلى البرنامج، فإن الـ traceback سيخبرك بالضبط ما الذي حدث — وهذه ميزة، لا عيب.
جملتا else و finally
تحتوي عبارة try على جملتين اختياريتين إضافيتين:
elseتُنفَّذ إذا انتهت كتلةtryدون إطلاق أي استثناء.finallyتُنفَّذ في كل الأحوال — سواء وقع استثناء أم لا.
العبارة else هي أنسب مكان لوضع الكود الذي يُنفَّذ عند النجاح فقط — فليس من الجيد أن تُحشر داخل try أشياء إضافية لا علاقة لها بالسطر الذي قد يُخفِق فعلاً. أما finally فمكانها عمليات التنظيف التي يجب أن تُنفَّذ سواء نجح try أو فشل: إغلاق مورد، تحرير قفل، أو إعادة حالة شيء ما إلى وضعه الأصلي.
إطلاق الاستثناءات يدوياً باستخدام raise في بايثون
استخدم raise للإشارة إلى وجود خطأ داخل الكود الخاص بك:
اختر نوع الاستثناء الذي يعكس طبيعة الخطأ فعلاً. ابدأ دائمًا بالأنواع الجاهزة في بايثون — مثل ValueError وTypeError وFileNotFoundError — قبل أن تفكّر في تعريف صنف خاص بك.
تعريف استثناء مخصص في بايثون
عندما لا تُعبّر الاستثناءات الجاهزة عن المعنى الذي تريده، يمكنك تعريف استثناء مخصص:
اشتق من Exception (أو من استثناء مدمج أكثر تخصصًا) وأضف docstring للصنف. هذا كل ما تحتاجه في الغالب. الاستثناءات المخصصة في بايثون تتيح للمستدعي التقاط الخطأ المتعلق بمجاله فقط دون غيره.
raise ... from ...: تسلسل الاستثناءات
حين يتسبب استثناء في إطلاق استثناء آخر، احرص على الحفاظ على السلسلة:
الـ from e يُرفق الخطأ الأصلي. وعند طباعة الـ traceback، تُظهر بايثون الاثنين معاً — الـ ConfigError الذي ظهر، والـ FileNotFoundError الذي تسبّب فيه. هذا النوع من التتبّع لا يُقدَّر بثمن أثناء تصحيح الأخطاء.
مديرو السياق: الطريقة الأنظف للتنظيف
صحيح أن finally يؤدي الغرض، لكن عند التعامل مع موارد كالملفات، يكون مدير السياق (وهو ما تعتمد عليه with) الخيار الأفضل دائماً تقريباً:
# finally version
f = open("data.txt")
try:
data = f.read()
finally:
f.close()
# with version
with open("data.txt") as f:
data = f.read()
كلا الأسلوبين آمن. صيغة with أقصر وتُطبَّق تلقائيًا. لا تلجأ إلى finally إلا عندما تقوم بعمل لم تغلّفه المكتبة القياسية أصلًا داخل مدير سياق.
متى لا تلتقط الاستثناء؟
التقاط الاستثناء قرار بحد ذاته؛ فأنت تقول ضمنيًا: "أنا قادر على التعامل مع هذا." وإن لم تكن كذلك، فدع الاستثناء يصعد للأعلى. الكود التالي يكاد يكون خطأً في كل الحالات:
try:
do_work()
except Exception:
pass # silently ignore everything
إخفاء الأخطاء يجعل العلل غير مرئية. من الأفضل أن يتعطل البرنامج بوضوح على أن يستمر في حالة غير متّسقة.
خلاصة القول
try/exceptيتيح لك التعامل مع الأخطاء التي يمكنك التعافي منها.- اصطَد الاستثناءات المحددة، لا
Exceptionبشكل عام. raiseيُستخدم للإشارة إلى الأخطاء في شيفرتك الخاصة.- كتل
withتُغني عن معظم عمليات التنظيف فيfinally. - عند الشك، دع الاستثناء يمرّ إلى الأعلى.
تالياً: جولة على أكثر الاستثناءات في بايثون شيوعاً — KeyError وValueError وModuleNotFoundError وغيرها — مع عادات تنقيح تساعدك على حلّها بسرعة.
الأسئلة الشائعة
كيف أتعامل مع الأخطاء في بايثون؟
ضع الكود الذي قد يُسبّب خطأ داخل كتلة try، ثم التقط الاستثناء المحدد في كتلة except، هكذا مثلًا: try: risky() except ValueError: .... كتلة else الاختيارية تنفَّذ فقط إذا لم يحدث أي استثناء، أما finally فتنفَّذ في جميع الأحوال وتستخدم عادةً لعمليات التنظيف.
هل من الأفضل التقاط Exception لتفادي أي مشكلة؟
لا، هذه عادة سيئة. استخدام except: بدون تحديد، أو except Exception: بشكل عام، يُخفي أخطاء برمجية لم تتوقعها ويجعل تشخيص المشاكل صعبًا. التقط فقط الاستثناء الذي تعرف كيف تتعامل معه، واترك ما عداه يصعد ليُظهر لك المشكلة الحقيقية.
ما الفرق بين raise و raise from؟
raise NewError(...) ترفع استثناءً جديدًا فقط. أما raise NewError(...) from original فتربط الاستثناء الأصلي بالاستثناء الجديد كسبب له، ويظهر ذلك بوضوح في الـ traceback. استخدم from عندما يتسبب خطأ من مستوى منخفض في إطلاق خطأ من مستوى أعلى تريد إبرازه للمستخدم.