الاستثناءات في بايثون

من موسوعة حسوب

قد تكون التعابير البرمجية في الشيفرة صحيحة من ناحية الصيغة، إلا أنّها قد تتسبب في حدوث أخطاء عند محاولة تنفيذها. تسمّى الأخطاء المُكتشفة أثناء تنفيذ الشيفرة بالاستثناءات (exceptions) وقد تتسبب في إيقاف عمل البرنامج (fatal) في بعض الأحيان.

تُنشئ الاستثناءات رسائل خطإٍ مماثلة لما يلي:

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly

هناك أنواع مختلفة من الاستثناءات، ويصف السطر الأخير في رسالة الخطأ نوع الاستثناء، ويعرض المثال السابق ثلاثة أنواع منها هي ZeroDivisionError و NameError و TypeError.

نوع الاستثناء المطبوع في رسالة الخطأ هو نفسه اسم الاستثناء الداخلي الذي جرى إطلاقه، وهذا ينطبق على جميع الاستثناءات الداخلية، ولكن قد لا ينطبق على الاستثناءات المعرّفة من قبل المستخدم (مع أنّ ذلك أمر يُنصح باتباعه)، وتجدر الإشارة هنا إلى أنّ أسماء الاستثناءات القياسية هي معرّفات داخلية وليست كلمات مفتاحية محجوزة.

للاطلاع على قائمة بالاستثناءات الداخلية يمكن مراجعة قسم الاستثناءات الداخلية.

التعامل مع الاستثناءات

يمكن التعامل مع الاستثناءات في بايثون باستخدام عبارة try-except والموضّحة في المثال التالي الذي يستمر في طلب المدخلات من المستخدم إلى أن تكون القيمة المدخلة عددًا صحيحًا، ولكنّه يسمح للمستخدم بمقاطعة البرنامج (بالضغط على مفتاحي Control-C أو أي طريقة أخرى يدعمها نظام التشغيل). لاحظ أنّ مقاطعة البرنامج من قبل المستخدم تؤدي إلى إطلاق استثناء من نوع KeyboardInterrupt.

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...

تعمل عبارة try بالطريقة التالية:

  • في البداية، يجري تنفيذ الشيفرة التي تقع ضمن كتلة try (أي الشيفرة التي تقع بين الكلمتين المفتاحيتين try و except).
  • في حال عدم حدوث أي استثناء، تتجاوز اللغة كتلة except وتنتهي بذلك عملية تنفيذ عبارة try.
  • في حال حدوث استثناء أثناء تنفيذ عبارة try، تتجاوز اللغة بقية الكتلة، فإن كان نوع الاستثناء الحاصل مطابقًا للاستثناء المسمّى بعد الكلمة المفتاحية except تنفّذ اللغة هذه العبارة ويستمر التنفيذ بعد عبارة try.
  • أما إن كان الاستثناء الحاصل غير مطابقٍ للاستثناء المسمّى بعد الكلمة المفتاحية except، يُمرَّر الاستثناء إلى عبارات try الخارجية، وفي حال عدم وجود أي عبارة خارجية يتحول الاستثناء إلى استثناء غير معالج unhandled exception وتتوقف عملية التنفيذ.

يمكن أن تتضمن عبارة try أكثر من عبارة except واحدة، وذلك لمعالجة مختلف أنواع الاستثناءات، وستنفّذ اللغة معالجًا واحدًا على الأكثر. تعالج معالِجات الأخطاء الاستثناءات التي تحدث في عبارة try المرتبطة بها، ولا تعالج الاستثناءات التي تحدث في المعالِجات الأخرى ضمن عبارة try نفسها. 

يمكن تسمية استثناءات متعددة في عبارة except على هيئة صف tuple محاط بالأقواس الهلالية، فمثلًا: 

... except (RuntimeError, TypeError, NameError):
...     pass

يكون الصنف في عبارة except متوافقًا مع الاستثناء إن كان الصنف نفسه أو صنفًا أساسيًّا منه (ولكن ليس بالعكس، فعبارة except التي تسرد صنفًا مشتقًّا لا تكون متوافقة مع الصنف الأساس). فعلى سبيل المثال ستطبع الشيفرة التالية B, C, D بهذا الترتيب: لاحظ أنّه لو قلبنا عبارات except (أي بجعل except B في البداية) فستطبع الشيفرة السابقة B, B, B، إذ تعمل أول عبارة except مطابقة فقط.

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")

قد تقوم عبارة except الأخيرة بحذف اسم الاستثناء، ويجب استخدامها بحذر شديد، لأنّها قد تغطّي على خطأ برمجي بهذه الطريقة. ويمكن أن تستخدم كذلك لطباعة رسالة خطأ ثم إعادة إطلاق الاستثناء (لتسمح لمستدعي الاستثناء بأن يعالجه أيضًا):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error: {0}".format(err))
except ValueError:
    print("Could not convert data to an integer.")
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise

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

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()

إنّ استخدام عبارة else هنا أفضل من إضافة شيفرة إضافية إلى عبارة try؛ وذلك لأنّها تمنع من التقاط استثناء - عن طريق الخطأ - لم تطلقه الشيفرة المحميّة بعبارة try ... except.

قد يمتلك الاستثناء قيمًا مرتبطة به عند حدوثه، وتعرف هذه القيم أيضًا بوسائط الاستثناء (exception's arguments)، ويعتمد وجود هذه الوسائط وطبيعتها على طبيعة الاستثناء.

يمكن لعبارة except أن تعيّن متغيّرًا بعد اسم الاستثناء، ويرتبط هذا المتغيّر بنسخة من الاستثناء  تخزن فيها قيم الوسائط في instance.args. تعرّف نسخة الاستثناء التابع ‎__str__()‎ لتسهيل طباعة الوسائط دون الحاجة للإشارة إلى ‎.args. ويمكن أيضًا تهيئة الاستثناء قبل إطلاقه وإضافة الخاصيات المطلوبة حسب الحاجة.

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # نسخة الاستثناء
...     print(inst.args)     # .args الوسائط المخزّنة في
...     print(inst)          # __str__ تتيح طباعة الوسائط بصورة مباشرة
...                          # ولكن يمكن إعادة تعريفها في الأصناف الفرعية للاستثناء
...     x, y = inst.args     # فك تحزيم الوسائط
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

تطبع الوسائط - إن كانت موجودة في الاستثناء - في الجزء الأخير من رسالة الخطأ الخاصة بالاستثناءات غير المعالَجة. لا تقتصر وظيفة معالجات الاستثناءات على معالجة الاستثناءات لحظة حدوثها في عبارة try، بل تتجاوز ذلك إلى معالجة تلك التي تحدث داخل الدوال التي تستدعى (ولو بصورة غير مباشرة) في عبارة try. فعلى سبيل المثال:

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as err:
...     print('Handling run-time error:', err)
...
Handling run-time error: division by zero

إطلاق الاستثناءات

تتيح عبارة raise إمكانية إطلاق الاستثناء المحدّد، فعلى سبيل المثال:

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: HiThere

يشير الوسيط الوحيد الذي تأخذه هذه العبارة إلى نوع الاستثناء المطلوب إطلاقه، ويجب أن يكون هذا الوسيط نسخة استثناء (exception instance) أو صنف استثناء (صنف مشتق من الصنف Exception). وفي حال تمرير صنف استثناء فإنّه سيهيّئ ضمنيًّا وذلك باستدعاء تابع المشيِّد دون تمرير أي وسائط:

raise ValueError  # 'raise ValueError()' اختصار للتعبير

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

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print('An exception flew by!')
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
NameError: HiThere

مصادر