تقييم الموضوع :
  • 0 أصوات - بمعدل 0
  • 1
  • 2
  • 3
  • 4
  • 5
الاستثناءات في بيثون
#1
الاستثناءات في بيثون
لعلك لاحظت في مواضيع سابقة استخدامنا لكتلة:

كود :
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'
"""
## قائمة بالاستثناءات المدمجة في بيثون تجدونها عبر الرابط التالي:  https://docs.python.org/3.6/library/exceptions.html

## القسمة على صفر ZeroDivisionError

سنبدأ بكتابة سكربت يحتوي على دالة بسيطة جدا تطلب وسيطين وتطبع نتيجة قسمة الاول على الثاني:

كود :
def div(a, b):
   print(a/b)
   print("Next...")
نحفظ هذا السكربت في ملف (division.py مثلا) ثم نشغله باستخدام المفتاح F5 (اذا كنت تستخدم Idle وإلا فانتقل بالطرفية حيث حفظت السكربت ثم شغل مفسر بيثون واستورد السكربت: from division import div

يمكننا التجربة:

كود :
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
"""
أطلق بيثون استثناء. نقرأ الاستثناء من الأسفل الى الأعلى: آخر سطر يعطي اسم الاستثناء ZeroDivisionError متبوعا بتوصيف قصير، وفي السطر ما قبل الأخير أرى التعليمة المتسببة في الاستثناء، ثم اسم الملف ورقم السطر اين نجد تلك التعليمة... نرى إذا أن بيثون عندما يطلق استثناء يقدم لنا عددا هاما من المعلومات المفيدة والتي تساعدنا في تحديد سبب الاستثناء واسمه وصولا الى رقم السطر في ملف السكربت المتسبب فيما حصل.

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

كود :
def div(a, b):
   try:
       print(a/b)
   except ZeroDivisionError:
       print("Warning: Division by zero.")
   print("Next...")
نحفظ الملف ثم نقوم باستيراده من جديد سواء باستخدام F5 على Idle او بواسطة التعليمة import كما هو مبين أعلاه

كود :
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'
"""
ما المشكلة؟ عندما نقرأ السطر الأخير نستطيع أن نعرف سبب الاستثناء وهو هذه المرة TypeError وهو اذا مختلف عن الاستثناء الذي عالجناه والمتعلق بالقسمة على صفر. يمكنني هنا ايضا التعامل مع هذا الاستثناء عبر اضافة تعليمة except

كود :
def div(a, b):
   try:
       print(a/b)
   except ZeroDivisionError:
       print("Warning: Division by zero.")
   except TypeError:
       print("Warning: Type error.")
   print("Next...")
نفهم من هذا أنه يمكننا اضافة التعليمة except أكثر من مرة للتعامل مع الاستثناءات المختلفة كل حسب طبيعتها:

كود :
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')
لاحظ بخصوص المثال الأخير أن المعلومة التي ردها الينا البرنامج غير صحيحة، اذ لا يتعلق الامر بقسمة على صفر إنما هو استثناء TypeError وهذا قد يتسبب في مشاكل يصعب اكتشافها فيما بعد. المنصوح به اذا هو حسن ادارة الاستثناءات التي يمكن ان تحدث في الكود وترك بيثون يرفع لنا الاستثناءات التي لم نستطع تحديد مصدرها.

## آلية الفقاقيع (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
"""
يوجد فائدتان هامتان لآلية الفقاقيع (bubbling أو stack trace): اولها هي امكانية اصطياد الاستثناء في أي مكان على طول الرصّة (stack)، والفائدة الثانية هي أن الاثر الذي يتركه الاستثناء في صعوده كالفقاقيع سيمنحني معلومات تساعد في تحديد العلل ومعالجتها. في المثال السابق لدي خيار تفحص السطر الثالث واصلاحه، لكنه ليس المتسبب المباشر في الاستثناء، او يمكنني الذهاب الى السطر رقم 8 او رقم 13 وسحب/تعديل/تصحيح التعليمة... آلية الفقاقيع او ما تسمى (stack trace) تتيح للمبرمج تتبع مصدر الاستثناء صعودا نحو مصدره الاول.

تعتبر معالجة الاستثناءات من بين الممارسات الجيدة عند البرمجة بلغة بيثون وخاصة عند معالجتها في أقرب مكان من الممكن أن تحدث فيه.

لكن كيف يمكنني معرفة اسماء وانواع الاستثناءات التي قد تحدث والتي يتوجب على المبرمج الجيد اصطيادها؟ في الواقع ليس هناك اجابة سحرية لهذا التساؤل، والطريقة الوحيدة لمعرفة الاستثناءات هي مراجعة دليل المكتبة المستخدمة. ذكرنا أعلاه رابطا نحو الاستثناءات المدمجة في بيثون، لكن من الوارد جدا أن نستخدم مكتبات أخرى اضافية لها استثناءاتها الخاصة. نضيف هنا مثالا آخر حول التعامل مع استثناء من نوع محدد:

كود :
def superf(n):
   persons = ['spam', 'egg', 'beacon']
   try:
       return persons[n]
   except IndexError:
       return None
نحفظ الملف ونشغله/نستورده. نستخدمه:

كود :
s = superf(100)
print(s)
كما نرى، لم يحدث انهيار مفاجئ للبرنامج رغم أن الوسيط 100 موجود خارج نطاق مؤشرات القائمة، لكن ماذا لو:

كود :
s = superf('1')         # TypeError
يمكننا تعديل الدالة:

كود :
def superf(n):
   persons = ['spam', 'egg', 'beacon']
   try:
       return persons[n]
   except (IndexError, TypeError):
       return None
لاحظ أنه يمكننا تجميع عدة انواع من الاستثناءات معا، او يمكننا التعامل مع كل نوع على حده: في المثال التالي لا معنى لقبض استثناء من نوع KeyError لأن الدالة لا تتعامل مع مفاتيح أصلا:

كود :
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
لاحظ أيضا أن الاستثناءات المدمجة في بيثون هي عبارة عن فئات (class) متوارثة عن بعضها البعض ضمن تسلسل هرمي (راجعها هنا: https://docs.python.org/3.6/library/exce...-hierarchy)
على سبيل المثال 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
"""
وسنجرب اطلاق استثناء لم نقرأ له حسابا: الكتلة finally سيتم تنفيذها مهما كانت الاحوال حتى في حالة انهيار البرنامج

كود :
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

يمكن للمبرمج تعمد اطلاق استثناء عبر التعليمة 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')
## التعليمة assert

تمنحنا هذه التعليمة طريقة للتأكد من تحقق شرط قبل المواصلة وهي في العادة مستخدمة داخل الكتلة 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

مع التنبيه الى أن للمكتبات الخارجية استثناءاتها الخاصة، كما يمكن لكل مبرمج تطوير استثناءات خاصة بالمكتبة التي طورها...
الرد


التنقل السريع :


مستخدمين يتصفحوا هذا الموضوع: 1 ضيف