AzLearn

أساسيات الاستثناءات

Exception Basics

مفهوم ~20 دقيقة

أساسيات الاستثناءات — Exception Basics

عندما يحدث خطأ في Python، اللغة لا تصمت — تُطلق استثناء (exception). الاستثناء هو إشارة تقول: “حدث شيء خاطئ، وإن لم يتعامل معه أحد سأوقف البرنامج.”

الفرق الجوهري بين Python ولغات مثل Go هو أن Python تعتمد على نموذج الاستثناءات (exception model): الخطأ يُرمى من مكان وتلتقطه في مكان آخر. هذا يجعل الكود أقصر في الحالات العادية، لكنه يتطلب وعياً بأن الدالة قد “تُفجّر” استثناءً في أي وقت.

في هذا الدرس ستتقن: كيف تلتقط الاستثناءات، كيف تُطلقها بنفسك، وما هي الاستثناءات المدمجة التي ستصادفها يومياً.

بنية try/except الأساسية

الهيكل الأساسي لمعالجة الاستثناءات في Python:

try:
    # الكود الذي قد يُطلق استثناء — Code that might raise an exception
    result = 10 / 0
except ZeroDivisionError:
    # يُنفَّذ عند حدوث الخطأ — Runs when the error occurs
    print("لا يمكن القسمة على صفر")

Python تُنفّذ كتلة try. إن حدث استثناء من النوع المُحدد في except، تنتقل التنفيذ مباشرة لكتلة except وتتجاهل بقية try. إن لم يحدث استثناء، تتخطى except كلياً.

الاستثناءات المدمجة الشائعة

Python تأتي بعشرات الاستثناءات المدمجة. هذه هي الأكثر مصادفة:

main.go

الربط بـ as e — تفاصيل الخطأ

عندما تكتب except SomeError as e، المتغير e يحمل كائن الاستثناء نفسه. يمكنك طباعته لرؤية الرسالة، أو الوصول لخصائصه.

try:
    int("xyz")
except ValueError as e:
    print(type(e))   # <class 'ValueError'>
    print(str(e))    # invalid literal for int() with base 10: 'xyz'
    print(e.args)    # ("invalid literal for int() with base 10: 'xyz'",)

e.args قائمة تحتوي كل الحجج التي أُرسلت للاستثناء عند إطلاقه. في معظم الحالات تحتوي عنصراً واحداً هو الرسالة.

التقاط استثناءات متعددة

أحياناً سلوك الخطأ متوقع من أنواع متعددة. لديك خياران:

الأسلوب الأول: كتل except منفصلة (مُفضَّل عند الحاجة لمعالجة مختلفة):

try:
    value = int(input_data)
    result = 100 / value
except ValueError:
    print("المدخل ليس رقماً")
except ZeroDivisionError:
    print("المدخل لا يمكن أن يكون صفراً")

الأسلوب الثاني: tuple في except واحد (عند نفس المعالجة):

try:
    process(data)
except (TypeError, ValueError) as e:
    print(f"مدخلات غير صالحة: {e}")

else و finally — التحكم الكامل

بنية try الكاملة في Python تحتوي 4 أجزاء:

main.go

else تُنفَّذ فقط عندما لا يحدث أي استثناء — مفيدة للتمييز بين “نجح الكود ونريد فعل شيء” و"نريد فعل هذا دائماً". finally تُنفَّذ دائماً حتى لو أُطلق استثناء لم نلتقطه، أو حتى لو استُدعي return داخل try — تستخدمها لتنظيف الموارد (إغلاق ملفات، قطع اتصالات).

إطلاق الاستثناءات بـ raise

لا تقتصر على التقاط الاستثناءات — يمكنك إطلاقها بنفسك عندما تكتشف حالة خاطئة:

def set_age(age):
    # التحقق من صحة العمر — Validate age
    if not isinstance(age, int):
        raise TypeError(f"العمر يجب أن يكون عدداً صحيحاً، وليس {type(age).__name__}")
    if age < 0 or age > 150:
        raise ValueError(f"العمر {age} خارج النطاق المعقول (0-150)")
    return age

raise تُطلق الاستثناء فوراً وتوقف التنفيذ الطبيعي. المستدعي يجب أن يلتقطه أو سيصل للمستخدم النهائي.

يمكنك أيضاً إعادة إطلاق استثناء جارٍ التعامل معه بكتابة raise بدون حجج:

try:
    risky_operation()
except ValueError as e:
    log_error(e)  # سجّل الخطأ — Log it
    raise         # أعد إطلاقه كما هو — Re-raise as-is

لماذا تجنب except Exception العامة

من أكثر الأخطاء الشائعة:

# ❌ خطير — Dangerous
try:
    do_something()
except Exception:
    print("حدث خطأ")  # ماذا حدث بالضبط؟ لا أحد يعرف!

هذا يلتقط كل شيء — حتى الأخطاء البرمجية التي يجب أن تُوقف البرنامج مثل AttributeError الناتج عن خطأ في الكود نفسه. النتيجة: برنامج “يعمل” لكنه يخفي أخطاء خطيرة.

القاعدة: التقط الاستثناء الأكثر تحديداً الممكن. إن كنت تتوقع ValueError التقط ValueError. إن كنت غير متأكد، اكتب الكود أولاً ثم راقب أي استثناءات تظهر في الواقع.

except Exception as e مقبول فقط في:

  • أعلى مستوى التطبيق (main loop) لتسجيل الأخطاء وإظهار رسالة للمستخدم
  • الكود الذي يُنفّذ callbacks خارجية لا تعرف نوع استثناءاتها

traceback — قراءة رسائل الخطأ

عندما لا تلتقط استثناءً، Python تطبع traceback كاملاً:

Traceback (most recent call last):
  File "app.py", line 15, in main
    result = process(data)
  File "app.py", line 8, in process
    return int(value)
ValueError: invalid literal for int() with base 10: 'abc'

اقرأه من الأسفل للأعلى: آخر سطر هو الخطأ الفعلي. الأسطر فوقه تتتبع مسار الاستدعاء (call stack) من الأعلى نحو الأسفل. السطر الأخير في الكتلة هو مكان الخطأ الحقيقي.

تطبيق: قراءة بيانات من قاموس بأمان

تحدي — Challenge

خلاصة

الاستثناءات في Python أداة قوية — لكنها تتطلب انضباطاً. التقط الأنواع المحددة، استخدم as e لتفاصيل الخطأ، واستخدم else للكود الذي يعتمد على نجاح try وfinally لتنظيف الموارد دائماً. إطلاق استثناءاتك الخاصة بـ raise يجعل دوالك تتواصل بوضوح مع المستدعي. في الدرس التالي ستتعلم كيف تبني استثناءات مخصصة تعبّر عن منطق تطبيقك.