معالجة الاستثناءات
درس 5 من 8
5معالجة الاستثناءات (Exception Handling)
الصورة الكبيرةكيف تلتقط الخطأ المتوقع فقط بدل ابتلاع كل شيء، والفرق بين except و else و finally، ولماذا الفشل الصامت أسوأ من الانهيار.
ماذا تشاهد في الرسم: الخطأ يولد في parse، ولا أحد يمسكه في load فيصعد، حتى يصل إلى main حيث ينتظره except فيتوقف هناك.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
الاستثناء مثل إنذار الحريق. لو أطفأت كل الإنذارات في المبنى لأنها مزعجة، لن تعرف بالحريق الحقيقي. الصواب أن تتعامل مع الإنذار الذي تتوقعه («المستخدم كتب حروفًا بدل رقم») وتترك الباقي يرنّ حتى يراه أحد.
try يحيط بالكود الذي قد يفشل، وexcept ValueError يلتقط نوعًا محددًا فقط، وelse يعمل إن لم يحدث خطأ، وfinally يعمل دائمًا للتنظيف. تجنّب except: العاري الذي يبتلع كل شيء حتى أخطاء البرمجة، وraise يعيد الخطأ أو يطلق خطأك الخاص.
قبل أن تبدأ
🧩الدوال
دالة تنادي دالة، وكل واحدة تعيد قيمة.
🔢التحويل بين الأنواع
int("5") يحوّل النص إلى رقم، وقد يفشل.
الفكرة الجوهرية
الخطأ أثناء التشغيل ليس نهاية البرنامج إن كنت تتوقّعه. هنا تتعلّم كيف تمسك الخطأ وتقرّر ماذا يحدث بعده، بثلاثة أمثلة صغيرة.
1امسك الخطأ بدل أن يوقفك
int("12") يحوّل النص إلى رقم. لكن int("اثنا عشر") لا يستطيع، فيرفع خطأً اسمه ValueError. ضع السطر الخطر داخل try، وما تريد فعله عند الخطأ داخل except.
text = "12"
print(int(text) + 1)
broken = "اثنا عشر"
try:
print(int(broken) + 1)
except ValueError:
print("هذا النص ليس رقمًا")
print("البرنامج ما زال يعمل")
السطر الأخير طُبع، أي أن البرنامج استمر بعد الخطأ بدل أن يتوقّف. هذا هو كل الهدف من الاستثناءات.
2أعد قيمة بديلة
القسمة على صفر ترفع ZeroDivisionError. داخل دالة، تستطيع أن تمسكه وتعيد None بمعنى «لا نتيجة»، ويبقى القرار لمن نادى الدالة.
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
print(divide(10, 2))
print(divide(1, 0))
لاحظ أننا سمّينا نوع الخطأ بالضبط. except بلا اسم يمسك كل شيء ويخفي عنك أخطاءً لم تتوقّعها، فتجنّبه.
3نظّف بيانات وسخة
النمط العملي الأكثر شيوعًا: قائمة فيها قيم بعضها صالح وبعضها لا. حاول تحويل كل قيمة، واحتفظ بما نجح، وسجّل ما فشل.
values = ["3", "x", "7"]
clean = []
for value in values:
try:
clean.append(int(value))
except ValueError:
print("تجاهلت:", value)
print(clean)
الـtry داخل الحلقة، لا حولها. لو وضعته حول الحلقة كلها، لتوقّفت عند أول قيمة سيئة وفقدت الباقي.
except: بلا نوع «حتى لا يتوقّف البرنامج». البرنامج فعلًا لن يتوقّف، لكنه سيبتلع كل الأخطاء بما فيها أخطاؤك الإملائية في أسماء المتغيّرات، وستقضي ساعة تبحث عن علّة كان بايثون سيخبرك بها في ثانية.كيف يعمل، خطوة بخطوة: رحلة الخطأ من مكانه إلى من يمسكه
سطر يفشل، مثل int("abc")، فيُصنع كائن خطأ.
بايثون يوقف الدالة فورًا ويبحث عن except يناسب نوعه.
إن لم يجده يصعد إلى الدالة التي نادتها، وهكذا.
أول except مناسب يمسكه، وإلا توقف البرنامج وطُبع Traceback.
أعمق قليلًا
البرنامج الاحترافي لا "ينهار" عند أول خطأ غير متوقع (مدخل خاطئ، ملف مفقود). معالجة الاستثناءات تتيح اكتشاف الخطأ والتعامل معه بأمان.
الرسم 5: else يُنفَّذ فقط عند نجاح try بلا أي استثناء، بينما finally يُنفَّذ في كل الحالات (نجاح أو فشل): مثالي لتنظيف الموارد.
| النوع | يحدث عادة عند |
|---|---|
ValueError | تحويل نص لا يمثّل رقمًا صحيحًا، مثل int("abc") |
TypeError | عملية بين أنواع غير متوافقة، مثل "5" + 5 |
IndexError | الوصول لعنصر خارج حدود List |
KeyError | الوصول لمفتاح غير موجود في Dict |
ZeroDivisionError | القسمة على صفر |
## مثال: تحويل مُدخل نصي لرقم بأمان
def convert_to_integer(text):
try:
value = int(text)
except ValueError:
print(f"'{text}' ليس رقمًا صحيحًا")
return None
else:
print("التحويل نجح") # يُنفَّذ فقط إن لم يحدث استثناء
return value
finally:
print("انتهت محاولة التحويل") # يُنفَّذ دائمًا مهما حدث
convert_to_integer("42") # نجح -> يطبع "التحويل نجح" ثم "انتهت..."
convert_to_integer("abc") # فشل -> يطبع رسالة الخطأ ثم "انتهت..."
## رفع استثناء صراحة عند حالة غير منطقية في عملك
def set_age(age):
if age < 0:
raise ValueError("العمر لا يمكن أن يكون سالبًا")
return age
except: عامة بدون تحديد نوع الاستثناء. هذا يُخفي أخطاء برمجية حقيقية (مثل خطأ إملائي في اسم متغيّر) بدل إصلاحها، لأنه "يبتلع" كل شيء بصمت.
اختبر فهمك: ما الفرق العملي بين وضع كود التنظيف داخل finally مقابل وضعه بعد الكتلة كاملة مباشرة؟
finally يضمن التنفيذ حتى لو حدث استثناء لم يُعالَج وتوقف البرنامج بعده لاحقًا (كإغلاق ملف قبل أن ينتشر الخطأ)، بينما كود بعد الكتلة مباشرة لن يُنفَّذ إطلاقًا إذا كان هناك استثناء لم تتم معالجته وانتشر للأعلى.
مثال محلول
try: age = int(text) ثم except ValueError: print("اكتب رقمًا"). امسك ValueError تحديدًا، لا كل الأخطاء، حتى لا تخفي خطأً آخر لم تتوقعه.خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| except وحدها بلا نوع أكثر أمانًا لأنها تمسك كل شيء. | تمسك حتى أخطاءك البرمجية وضغطة Ctrl+C، فتخفي المشكلة بدل حلّها. امسك النوع الذي تتوقعه. |
| الاستثناء يعني أن البرنامج سيئ. | الاستثناء طريقة بايثون في قول «لم أستطع»، مثل ملف غير موجود. البرنامج الجيد يتوقعه ويتعامل معه. |
| finally تعمل فقط إذا حدث خطأ. | تعمل دائمًا، نجح الكود أو فشل، ولهذا مكانها للتنظيف مثل إغلاق اتصال. |
جرّب بنفسك
كتبت try: x = int("5") ثم except ValueError: x = 0. ما قيمة x؟
التحويل نجح لأن "5" نص صالح لرقم، فلم يُرفع أي خطأ ولم يدخل البرنامج في except أصلًا. كتلة except تعمل فقط عند حدوث الخطأ المذكور، وإلا تُتجاهل تمامًا.
بطاقات مراجعة سريعة
تحقق من فهمك
متى يعمل فرع else في try؟
عندما ينجح ما في try دون أي خطأ.
لماذا تمسك ValueError بدل Exception؟
لأنك تعرف ماذا تفعل معه، والأخطاء الأخرى يجب أن تظهر حتى تصلحها.
دالة لا تمسك الخطأ: أين يذهب؟
يصعد إلى الدالة التي نادتها، حتى يمسكه أحد أو يتوقف البرنامج.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي3
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.