12  الاستثناء

الاستثناء (Exception) هو خروج البرنامج عن المسار المثالي. ويسمى أيضًا الخطأ العملي (Runtime Error). مثل:

  1. أن يؤمَر بقراءة ملف .. والواقع أن هذا الملف غير موجود!
  2. أو أن يطلب من المستخدم رقمًا فيعطيه كلاماً!
  3. أو أن يطلب من الشبكة شيئًا .. فتنقطع الشبكة!

فكل هذه تعتبر مسارات غير مثالية لكنها تحصل في ظروف واقعيَّة. فيجب كتابة قطع في البرنامج تتعامل معها. ولذا فإن بعض الممارسين لا يفضلون استعمال كلمة استثناء لأنَّ مثل ذلك يحصل كثيرًا فهو ليس خارجًا عن العادة؛ بل من الطبيعي أن يحصل ذلك في الواقع!

تغير سير الأوامر عند الخطأ

تنفذ التعليمات في لغة البرمجة الأمرية (Imperative) كبايثون بحسب ترتيبها (من الأعلى إلى الأسفل). لكن عند حدوث خطأ، يتغيَّر سيْر الأوامر باستعمال جملة try-except. وشكل جملة التعامل مع الخطأ على هذا النحو:

  • المحاولة: try تتضمن الجملة التي نتوقع حدوث خطأٍ فيها
  • المطابقة: except Exception هي مثل if تنفذ ما تتضمنه إن كان الخطأ من نوع Excpetion
  • عدم المطابقة: else تعمل عند عدم الخطأ
  • المتابعة: finally وهي جُملة تعمل سواء وقع الخطأ أم لم يقع؛ لكنَّ بايثون تضمن عملها إن حصل خطأ أثناء التعامل مع الخطأ
print('enter')
try:
    # حاول تشغيل هذه القطعة
except Exception as e:
    # شغل هذه القطعة عند الخطأ
else:
    # وإن لم يحصل فهذه القعطة
finally:
    # وهذه تشتغل سواء حصل الخطأ أم لا
    # وفائدتها أنها تعمل قبل رجوع الخطأ لموضع النداء
print('exit')

أنواع الاستثناء

تم تعريف أنواع من الخطأ في بايثون متبوعة بكلمة Error، وذلك باعتبار حالات خطأ نمطية ومتكررة:

SyntaxError

السبب: خطأ نحوي في صياغة اللغة:

  • كلمة غير صحيحة: خطأ في الإملاء
  • في وضع كلمة صحيحة في غير سياقها
  • محاذاة غير متسقة (IndentationError)

الحل: اقرأ رسالة الخطأ وستدلُّك على السبب والموضع الذي حصل فيه الخطأ.

TypeError

السبب:

  1. طلب فعل بعدد أكثر أو أقل من العوامل الواجبة (مثل: len(1, 2))
  2. طلب فعل بعوامل لا تطابق النوع المحدد في تعريفه (مثل: math.sqrt('nine') أو 5 + '5')

الحل: الوقاية بـ type() أو isinstance() أو بالتأكد من تحويل النوع مسبقًا.

a = 5
b = input('Enter a number: ')
result = a + int(b)

ValueError

السبب: أن يكون النوع صحيحًا (فلا يحصُل TypeError) لكن القيمة غير مقبولة.

  • مثلاً: طلب فعل بقيمة نوعها عددي لكنَّها سالبة وهو لا يقبل إلا الموجبة. نحو: math.sqrt(-16) فالجذر التربيعي لا يقبل السالب.

الحل: الوقاية بفحص مدى القيمة ، نحو: if x >=0: math.sqrt(x)

IndexError & KeyError

السبب: الرقم الذي استعمل في عملية الإشارة [index]list (قائمة) أو dict[key] (قاموس) يشير لما هو خارج المجموعة.

نحو:

my_list = [10, 20, 30]
idx = 3
value = my_list[idx]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[1], line 3
      1 my_list = [10, 20, 30]
      2 idx = 3
----> 3 value = my_list[idx]

IndexError: list index out of range

الحل بالوقاية:

if idx < len(my_list):
    value = my_list[idx]
else:
    # do something else

أو بالعلاج:

try:
    value = my_list[idx]
except IndexError:
    # do something else

وكذلك في القاموس، نحو:

my_dict = {'A': 10, 'B': 20, 'C': 30}
key = 'Z'
value = my_dict[key]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[2], line 3
      1 my_dict = {'A': 10, 'B': 20, 'C': 30}
      2 key = 'Z'
----> 3 value = my_dict[key]

KeyError: 'Z'

الحل بالوقاية

if key in my_dict:
    value = my_dict[key]
else:
    # do something else

أو هكذا (تعيين قيمة ابتدائية):

value = my_dict.get(key, 0)

أو بالعلاج:

try:
    value = my_dict[key]
except KeyError:
    # do something else

AttributeError & NameError

السبب: استعمال متغير أو فعل قبل تعريفه.

  • فإن أسنِد إلى كائن؛ وقع AttributeError
  • وإلا وقع NameError
a = 10
a + X
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 2
      1 a = 10
----> 2 a + X

NameError: name 'X' is not defined
some_function(55)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 some_function(55)

NameError: name 'some_function' is not defined
class A:
    pass

a = A()
a.x
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[5], line 5
      2     pass
      4 a = A()
----> 5 a.x

AttributeError: 'A' object has no attribute 'x'
a.do_something()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[6], line 1
----> 1 a.do_something()

AttributeError: 'A' object has no attribute 'do_something'

ModuleNotFoundError

السبب: فشل جُملة الاستيراد import numpy

الحل:

  • تأكد من صحة الإملاء
  • تأكد من تثبيت الوحدة في البيئة التي يعمل فيها البرنامج: pip install numpy

تعريف أخطاء جديدة

تعريف الخطأ يكون بتعريف نوع جديد يرث من النوع Exception، وهذا ما يحققه السطر الأول بين القوسين. وتستطيع أن ترث ممن يرث، فتتكون لديك فروع من هذا الخطأ:

class ParentError(Exception):
    pass

class XError(ParentError):
    pass

class YError(ParentError):
    pass

flowchart TD
    A[ParentError] --> B[XError]
    A --> C[YError]

وهذا الإجراء يحصل فيه الخطأ بطريقة مصطنعة لكنها توضح ما نريد، وهو الخطأ الفرعي XError الذي يرث من الخطأ الأصلي ParentError:

def do_something():
    raise XError('Something went wrong')

ثم حين نفحص، تسطيع أن نطابق بالأصل أو الفرع:

try:
    do_something()
except ParentError as e:
    print("caught you:", e)
caught you: Something went wrong

المصطلحات

العربية الإنجليزية
الاستثناء Exception
خطأ Error
ظرف التنفيذ Execution Frame
الوقاية If-else
العلاج Try-except
خطأ نحوي Syntax Error
خطأ في المحاذاة Indentation Error
خطأ في النوع Type Error
خطأ في القيمة Value Error
خطأ في المؤشر الرقمي Index Error
خطأ في المؤشر المرقوم Key Error
خطأ في الخاصية Attribute Error
خطأ في الاسم Name Error
خطأ في استيراد الوحدة Module Not Found Error
خطأ القسمة على صفر Zero Division Error
رفع الخطأ Raise Exception
إلتقاط الخطأ Catch Exception
تدفق الخطأ Error Flow