الاستثناءات في بايثون
قد تكون التعابير البرمجية في الشيفرة صحيحة من ناحية الصيغة، لكن قد يؤدي تنفيذ تلك الشيفرة إلى التسبب في حدوث الأخطاء. تسمّى الأخطاء المُكتشفة أثناء تنفيذ الشيفرة بالاستثناءات (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
يمكن التعامل مع الاستثناءات في بايثون باستخدام عبارة 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
متوافقةً مع الاستثناء إن كانت من أصناف الاستثناء أو صنفًا أساسيًّا base class منه (ولكن ليس بالعكس، فعبارة except
التي تسرد صنفًا مشتقًّا ليست متوافقة مع صنف أساسي). فعلى سبيل المثال ستطبع الشيفرة التالية B, C, D
بهذا الترتيب:
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
(أي بجعل except B
في البداية) فستطبع الشيفرة السابقة B, B, B
، إذ ستعمل عبارة except
الأولى لأنّ الصنفين C
و D
مشتّقّان من الصنف B
.
يمكن حذف اسم الاستثناء من عبارة except
الأخيرة، ولكن يجب توخّي الحذر عندئذٍ، إذ يمكن وبكل سهولة التغطية على خطأ برمجي حقيقي بهذه الطريقة.
يمكن أيضًا استخدام عبارة 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
انظر أيضًا
- أخطاء الصيغة في بايثون.
- الاستثناءات المضمنة داخليًا في بايثون.
- الاستثناءات المعرفة من طرف المستخدم.
- تعريف أحداث التنظيف clean-up actions.
مصادر
- صفحة Errors and Exceptions في توثيق بايثون الرسمي.