الاستثناءات في بيثون
لعلك لاحظت في مواضيع سابقة استخدامنا لكتلة:كود :
try:
.......
except .....:
.......
- أولا الاستثناءات ليست خطأ قاتلا في البرنامج، هي آلية إعلام عن أخطاء في البرنامج حيث نستطيع 'اصطياد' استثناء والتعامل معه.
- ثانيا تزودنا الاستثناءات بمعلومات عن الأخطاء التي تحدث، فهي بالتالي آلية تثبت من الأخطاء مفيدة للغاية
- ثالثا وحيث أن الاستثناءات في بيثون ذات فاعلية كبيرة، فهي آلية مستخدمة بكثرة خلال العمل العادي للبرنامج
سنستخدم في امثلتنا محرر نصوص عوض استخدام المفسر مباشرة نظرا لأننا سنكتب عدة سطور قبل تنفيذها: المحرر IDLE مناسب جدا لهذا الغرض.
## ما هو الاستثناء؟
هو آلية لتوقيف المسار العادي للبرنامج للاشارة الى أن شيئا غير طبيعيا بصدد الحدوث. تستخدم الاستثناءات عادة للتبليغ عن خطأ في البرنامج. على سبيل المثال سيطلق بيثون استثناء في كل حالة من الحالات التالية:
كود :
x = 1 / 0 # القسمة على صفر
"""
ZeroDivisionError: division by zero
"""
l = [1, 2, 3]
l[100] # مؤشر خارج القائمة
"""
IndexError: list index out of range
"""
d = {'key':'value'}
d['nope'] # مفتاح غير موجودفي القاموس
"""
KeyError: 'nope'
"""
s = 'banana' + 1 # عملية بين نوعين غير متساوقين
"""
TypeError: Can't convert 'int' object to str implicitly
"""
import kanaw # وحدة لا وجود لها
"""
ImportError: No module named 'kanaw'
"""
open('awax') # ملف لا وجود له
"""
FileNotFoundError: [Errno 2] No such file or directory: 'awax'
"""
print(banana) # متغير غير معرّف
"""
NameError: name 'banana' is not defined
"""
$var = 123 # خطأ في تسمية المتغير
"""
SyntaxError: invalid syntax
"""
int('a') # محاولة تحويل غير صحيحة
"""
ValueError: invalid literal for int() with base 10: 'a'
"""
## القسمة على صفر ZeroDivisionError
سنبدأ بكتابة سكربت يحتوي على دالة بسيطة جدا تطلب وسيطين وتطبع نتيجة قسمة الاول على الثاني:
كود :
def div(a, b):
print(a/b)
print("Next...")
يمكننا التجربة:
كود :
div(1, 2)
"""0.5
Next..."""
كود :
div(1, 0)
"""
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/..../division.py", line 2, in div
print(a/b)
ZeroDivisionError: division by zero
"""
لاحظ أيضا أن السطر Next... لم يتم طبعه على الشاشة، أي أن السكربت توقف قبل تنفيذ هذا السطر. في سكربتنا السابق لم نأخذ ما يلزم من احتياطات، لذلك يتوقف البرنامج بشكل مفاجئ وغير طبيعي، غير أنه يمكننا اصطياد ذلك الاستثناء ومعالجته دون التسبب في التوقف المفاجئ للبرنامج: سنعود للدالة التي كتبناها ونضيف كتلة (try except) تتيح اصطياد الاستثناءات:
كود :
def div(a, b):
try:
print(a/b)
except ZeroDivisionError:
print("Warning: Division by zero.")
print("Next...")
كود :
div(1, 2)
"""0.5
Next..."""
div(1, 0)
"""Warning: Division by zero.
Next..."""
## الاستثناء TypeError
سنرى الآن كيف سيتصرف برنامجنا عند قسمة عدد على كائن آخر غير الأعداد، وهو أمر قد يحدث في البرامج الكبيرة عندما نعالج كائنات مختلفة الانواع
كود :
div(1, '0')
"""
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/.../division.py", line 3, in div
print(a/b)
TypeError: unsupported operand type(s) for /: 'int' and 'str'
"""
كود :
def div(a, b):
try:
print(a/b)
except ZeroDivisionError:
print("Warning: Division by zero.")
except TypeError:
print("Warning: Type error.")
print("Next...")
كود :
div(1, 2)
div(1, 0)
div(1, '0')
في بيثون يمكن استخدام تعليمة except دون تحديد نوعها، لكن هذا ليس تصرفا جيدا لأنه سيخفي الاستثناءات التي تحدث بحيث لا يعرف المبرمج أو المستخدم النهائي مصدرها بسهولة أو أسوأ من ذلك، قد تعطيه معلومة غير صحيحة عن سبب الاستثناء:
كود :
def div(a, b):
try:
print(a/b)
except:
print("Warning: Division by zero")
print("Next...")
كود :
div(1, 2)
div(1, 0)
div(1, '0')
## آلية الفقاقيع (bubbling)
هناك خاصية مهمة في الاستثناءات هي آلية الفقاقيع (bubbling أو stack trace) أي أنها ترتفع من آخر قطعة كود متسببة في الاستثناء الى الكود الذي استدعاها، وهكذا على طول الرصّة (stack) الى أن يتوقف البرنامج:
كود :
def div(a, b):
print("in div(a, b)...")
print(a/b)
print("out div(a, b)...")
def f(x):
print("in f(x)...")
div(1,x)
print("out f(x)...")
def g(x):
print("in g(x)...")
f(x)
print("out g(x)...")
كود :
g(2)
"""
in g(x)...
in f(x)...
in div(a, b)...
0.5
out div(a, b)...
out f(x)...
out g(x)...
"""
كود :
g(0)
"""
in g(x)...
in f(x)...
in div(a, b)...
Traceback (most recent call last):
File "<pyshell#3>", line 1, in <module>
g(0)
File "/.../division.py", line 13, in g
f(x)
File "/.../division.py", line 8, in f
div(1,x)
File "/.../division.py", line 3, in div
print(a/b)
ZeroDivisionError: division by zero
"""
تعتبر معالجة الاستثناءات من بين الممارسات الجيدة عند البرمجة بلغة بيثون وخاصة عند معالجتها في أقرب مكان من الممكن أن تحدث فيه.
لكن كيف يمكنني معرفة اسماء وانواع الاستثناءات التي قد تحدث والتي يتوجب على المبرمج الجيد اصطيادها؟ في الواقع ليس هناك اجابة سحرية لهذا التساؤل، والطريقة الوحيدة لمعرفة الاستثناءات هي مراجعة دليل المكتبة المستخدمة. ذكرنا أعلاه رابطا نحو الاستثناءات المدمجة في بيثون، لكن من الوارد جدا أن نستخدم مكتبات أخرى اضافية لها استثناءاتها الخاصة. نضيف هنا مثالا آخر حول التعامل مع استثناء من نوع محدد:
كود :
def superf(n):
persons = ['spam', 'egg', 'beacon']
try:
return persons[n]
except IndexError:
return None
كود :
s = superf(100)
print(s)
كود :
s = superf('1') # TypeError
كود :
def superf(n):
persons = ['spam', 'egg', 'beacon']
try:
return persons[n]
except (IndexError, TypeError):
return None
كود :
def superf(n):
persons = ['spam', 'egg', 'beacon']
try:
return persons[n]
except (IndexError, TypeError):
return None
except KeyError:
# This exception does nothing here!
return None
على سبيل المثال IndexError و KeyError ترثان من LookupError. نستطيع اذا تعديل الدالة السابقة:
كود :
def superf(n):
persons = ['spam', 'egg', 'beacon']
try:
return persons[n]
except (LookupError, TypeError):
return None
## مزيد من التوسع try ... else ... finally
تكون التعليمة try متبوعة عادة بتعليمة أو أكثر except كما رأينا أعلاه، لكن يوجد أيضا:
- تعليمة else يتم تنفيذها اذا لم تحدث استثناءات (بمعنى أن البرنامج عمل بشكله المنتظر)
- تعليمة finally يتم تنفيذها في كل الاحوال
التعليمة finally ربما هي الاكثر فائدة من بين هاتين التعليمتين لأنها تسمح مثلا بعمل تنظيف في كل الحالات (الا في حالة قطع الكهرباء ربما...)، والتعليمات الموجودة في كتلتها سيتم تنفيذها حتى لو استخدمنا return وحتى لو لم نعالج الاستثناء وانظر الى الترتيب:
كود :
def superf(n):
persons = ['spam', 'egg', 'beacon']
try:
return persons[10/n] # تعديل متعمد
except (LookupError, TypeError):
return None
else:
print("Something is wrong?")
finally:
print("Cleaning ... Please wait")
كود :
print(superf(1))
"""
Cleaning ... Please wait
egg
"""
كود :
print(superf(0))
"""
Cleaning ... Please wait
............................
ZeroDivisionError: division by zero
"""
البرنامج الأمثل يعالج الاستثناءات في جميع الحالات، لكن هذا صعب التطبيق. هناك استثناءات تحدث بسبب عوامل من خارج كود البرنامج كاستحالة كتابة ملف او انقطاع الاتصال بالانترنت بالنسبة الى برنامج يتعامل مع الوب مثلا... والاسباب عديدة لا يكون للبرنامج ولا للمبرمج قدرة التحكم فيها.
كود :
import requests
from PIL import Image
from io import BytesIO
def image_downloader(url):
r = requests.get(url)
myLogo = Image.open(BytesIO(r.content))
myLogo.show()
كود :
image_downloader('https://forums.wahaproject.org/uploads/logo.png')
هذا تعديل مبسط للسكربت يعالج بعض الاستثناءات التي يمكن أن تحدث
كود :
try:
import requests
from PIL import Image
from io import BytesIO
except ImportError:
print("One module at least is missing.")
print("Please check that the modules listed above are installed.")
def image_downloader(url):
try:
print("Requesting url...")
r = requests.get(url)
print("Opening image...")
myLogo = Image.open(BytesIO(r.content))
print("Showing image..")
myLogo.show()
except ConnectionError:
print("A network problem has occurred ....")
except IOError:
print("File cannot be found.")
except:
print("Unknown error...")
else:
print("No error... Good")
finally:
print("cleaning...")
كود :
url = 'https://forums.wahaproject.org/uploads/logo.png'
image_downloader(url)
كود :
url = 'https://forums.wahaproject.org/uploads/loo.png'
image_downloader(url)
كود :
import requests
from PIL import Image
from io import BytesIO
def image_downloader(url):
try:
r = requests.get(url)
myLogo = Image.open(BytesIO(r.content))
myLogo.show()
except Exception as err:
print("An error has occurred.")
print("Show 'errors.log' for more informations.")
with open("errors.log", "w") as f:
f.write("{} :\n{}".format(type(err).__name__, err.args))
كود :
image_downloader('https://forums.wahaproject.org/uploads/loo.png')
يمكن للمبرمج تعمد اطلاق استثناء عبر التعليمة raise متبوعة باسم يشير الى استثناء. في المثال السابق رغم أن العنوان قد يكون خاطئا (404) لكن لا يطلق بيثون خطأ الا بعد محاولة فتح الصورة. يمكننا مثلا برمجة استثناء اذا كان كود HTTP مختلفا عن 200 (OK). في المثال اعتبرته من نوع FileNotFoundError لكن لاشيء يمنع من اعتبار الاستثناء من نوع آخر ValueError مثلا:
كود :
import requests
from PIL import Image
from io import BytesIO
def image_downloader(url):
try:
r = requests.get(url)
if r.status_code != 200:
raise FileNotFoundError("{}: File not found".format(r.status_code))
myLogo = Image.open(BytesIO(r.content))
myLogo.show()
except Exception as err:
print("An error has occurred.")
print("Show 'errors.log' for more informations.")
with open("errors.log", "w") as f:
f.write("{} :\n{}".format(type(err).__name__, err.args))
كود :
image_downloader('https://forums.wahaproject.org/uploads/loo.png')
تمنحنا هذه التعليمة طريقة للتأكد من تحقق شرط قبل المواصلة وهي في العادة مستخدمة داخل الكتلة try except يمكننا تعديل السكربت السابق كما يلي:
كود :
import requests
from PIL import Image
from io import BytesIO
def image_downloader(url):
try:
r = requests.get(url)
assert r.status_code == 200, "HTTP request != 200"
myLogo = Image.open(BytesIO(r.content))
myLogo.show()
except Exception as err:
print("An error has occurred.")
print("Show 'errors.log' for more informations.")
with open("errors.log", "w") as f:
f.write("{} :\n{}".format(type(err).__name__, err.args))
كود :
image_downloader('https://forums.wahaproject.org/uploads/loo.png')
https://docs.python.org/3/tutorial/error...exceptions
https://docs.python.org/3/library/exceptions.html
مع التنبيه الى أن للمكتبات الخارجية استثناءاتها الخاصة، كما يمكن لكل مبرمج تطوير استثناءات خاصة بالمكتبة التي طورها...