Skip to content

5. الاستثناءات

ننتقل الآن إلى الاستثناءات.

Image

5.1. نص برمجي [exceptions_01]

يوضح البرنامج النصي الأول ضرورة التعامل مع الاستثناءات.

# إحداث خطأ متعمد
x = 4 / 0

نقوم بإحداث خطأ متعمد لنرى ما سيحدث (exceptions-01.py):

1
2
3
4
5
6
7
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_01.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_01.py", line 2, in <module>
    x = 4 / 0
ZeroDivisionError: division by zero

Process finished with exit code 1

يُظهر لنا السطر 4:

  • نوع الاستثناء: ZeroDivisionError؛
  • رسالة الخطأ المرتبطة: division by zero. وهي باللغة الإنجليزية. وهذا أمر قد نرغب في تغييره.

من القواعد الأساسية أنه يجب بذل كل جهد ممكن لتجنب الاستثناءات التي ينتجها مترجم Python. علينا إدارة الأخطاء بأنفسنا.

صيغة معالجة الاستثناءات هي كما يلي:


try:
    actions susceptibles de lancer une exception
except (Ex1, Ex2…) as ex:
    actions de gestion de l'exception [ex]
except (Ex11, Ex12…) as ex:
    actions de gestion de l'exception [ex]
finally:
   actions toujours exécutées qu'il y ait exception ou non

في جملة try، يتوقف تنفيذ الإجراءات فور حدوث استثناء (خطأ). في هذه الحالة، يستمر التنفيذ مع الإجراءات الواردة في إحدى جمل except:

  • السطر 3: إذا كان الاستثناء [ex] الذي حدث من نوع ينتمي إلى التوبول (Ex1، Ex2…) أو مشتقًا من أحدهما، فسيتم تنفيذ الإجراءات الواردة في السطر 4؛
  • السطر 5: إذا لم يتم اعتراض الاستثناء بواسطة السطر 3، وكان هناك جملة [except] أخرى، فإن العملية نفسها تتكرر. وهكذا دواليك…؛
  • يمكن أن يكون هناك أي عدد من البنود [except] حسب الحاجة لإدارة أنواع الاستثناءات المختلفة التي قد تحدث في [try]؛
  • إذا لم تتم معالجة الاستثناء بواسطة أي من البنود [except]، فسيتم تمريره إلى الكود المستدعي. وإذا كان هذا الكود المستدعي موجودًا بدوره داخل بنية try / except، فسيتم معالجة الاستثناء مرة أخرى؛ وإلا فسيستمر في التمرير عبر سلسلة الطرق المستدعاة. وفي نهاية المطاف، تصل الاستثناءات إلى مترجم Python. عندئذٍ يقوم المترجم بإيقاف البرنامج قيد التنفيذ وعرض رسالة خطأ من النوع الموضح في المثال السابق. وبالتالي، فإن القاعدة هي أن البرنامج الرئيسي يجب أن يلتقط جميع الاستثناءات التي قد تنتقل من الطرق المستدعاة؛
  • السطر 7: يتم دائمًا تنفيذ الجملة [finally] سواء حدث استثناء (متابعة جملة except) أم لا (متابعة جملة try). وينطبق هذا حتى لو حدث استثناء ولم يتم اعتراضه. في هذه الحالة، سيتم تنفيذ الجملة [finally] قبل أن تنتقل الاستثناء إلى الكود المستدعي؛

يحمل الاستثناء معه معلومات عن الخطأ الذي حدث. ويمكن الحصول عليها باستخدام الصيغة التالية:


except MyException as exception:

[exception] هي الاستثناء الذي حدث. [exception.args] تمثل مجموعة معلمات الاستثناء.

لإطلاق استثناء، نستخدم الصيغة


raise MyException(param1, param2…)

حيث غالبًا ما تكون MyException فئة مشتقة من الفئة BaseException. وستكون المعلمات التي تم تمريرها إلى منشئ الفئة متاحة في جملة «except» في هياكل اعتراض الاستثناءات باستخدام الصيغة [ex.args] إذا كانت [ex] هي الاستثناء الذي تم اعتراضه بواسطة جملة [except].

يتم توضيح هذه المفاهيم من خلال البرنامج النصي التالي.

5.2. نص برمجي [exceptions_02]

يعالج البرنامج النصي التالي الأخطاء بشكل صريح:


# معالجة الاستثناءات
essai = 0

# إحداث خطأ ومعالجته
x = 2
try:
    x = 4 / 0
except ZeroDivisionError as erreur:
    # الخطأ هو الاستثناء الذي تم اعتراضه
    print(f"essai n° {essai} : {erreur}")
# لم تتغير قيمة x
print(f"x={x}")

# نعيد المحاولة
essai += 1
try:
    x = 4 / 0
except BaseException as erreur:
    # يتم اعتراض الاستثناء الأكثر عمومية
    # الخطأ هو الاستثناء الذي تم اعتراضه
    print(f"essai n° {essai} : {erreur}")

# يمكن اعتراض أنواع مختلفة من الاستثناءات
# يتوقف التنفيذ عند أول [except] قادر على معالجة الاستثناء
essai += 1
try:
    x = 4 / 0
except ValueError as erreur:
    # هذا الاستثناء لا يحدث هنا
    print(f"essai n° {essai} : {erreur}")
except BaseException as erreur:
    # يتم اعتراض الاستثناء الأكثر عمومية
    print(f"essai n° {essai} : (Exception) {erreur}")
except ZeroDivisionError as erreur:
    # يتم اعتراض نوع محدد
    print(f"essai n° {essai} : (ZeroDivisionError) {erreur}")

# نعيد المحاولة بتغيير الترتيب 
essai += 1
try:
    x = 4 / 0
except ValueError as erreur:
    # هذا الاستثناء لا يحدث هنا
    print(f"essai n° {essai} : {erreur}")
except ZeroDivisionError as erreur:
    # يتم اعتراض نوع محدد
    print(f"essai n° {essai} : (ZeroDivisionError) {erreur}")
except BaseException as erreur:
    # يتم اعتراض الاستثناء الأكثر عمومية
    print(f"essai n° {essai} : (Exception) {erreur}")

# جملة «except» بدون معلمات
essai += 1
try:
    x = 4 / 0
except:
    # لا نهتم بطبيعة الاستثناء
    print(f"essai n° {essai} : il y a eu un problème")

# نوع آخر من الاستثناءات
essai += 1
try:
    # لا يمكن تحويل x إلى عدد صحيح
    x = int("x")
except ValueError as erreur:
    # erreur هو الاستثناء الذي تم اعتراضه
    print(f"essai n° {essai} : {erreur}")

# يحتوي الاستثناء على معلومات في مجموعة بيانات يمكن للبرنامج الوصول إليها
essai += 1
try:
    x = int("x")
except ValueError as erreur:
    # الخطأ هو الاستثناء الذي تم اعتراضه
    print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")

# يمكن إلقاء الاستثناءات
essai += 1
try:
    raise ValueError("param1", "param2", "param3")
except ValueError as erreur:
    # الخطأ هو الاستثناء الذي تم اعتراضه
    print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")


# يمكن إنشاء استثناءات خاصة
# يجب أن تكون مشتقة من الفئة [BaseException]
class MyError(BaseException):
    pass


# يتم إطلاق الاستثناء MyError
essai += 1
try:
    raise MyError("info1", "info2", "info3")
except MyError as erreur:
    # الخطأ هو الاستثناء الذي تم اعتراضه
    print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")

# يتم إلقاء الاستثناء MyError مع رسالة خطأ
essai += 1
try:
    raise MyError("mon msg d'erreur")
except MyError as erreur:
    # الخطأ هو الاستثناء الذي تم اعتراضه
    print(f"essai n° {essai} : {erreur.args[0]}")

# يتم دائمًا تنفيذ جملة finally
# سواء حدثت استثناء أم لا
essai += 1
x = None
try:
    x = 1
except:
    # استثناء
    print(f"essai n° {essai} : exception")
finally:
    # يتم تنفيذه في جميع الحالات
    print(f"essai n° {essai} : finally x={x}")

essai += 1
x = None
try:
    x = 2 / 0
except:
    # استثناء
    print(f"essai n° {essai} : exception")
finally:
    # يتم تنفيذه في جميع الحالات
    print(f"essai n° {essai} : finally x={x}")

# ليس من الضروري وضع جملة [except]
essai += 1
try:
    # يؤدي إلى حدوث خطأ
    x = 4 / 0
finally:
    # يتم تنفيذه في جميع الحالات
    print(f"essai n° {essai} : finally x={x}")

ملاحظات:

  • الأسطر 4-12: يتم التعامل مع القسمة على الصفر؛
  • السطر 8: يتم اعتراض الاستثناء المحدد الذي حدث وعرضه؛
  • السطر 12: بسبب الاستثناء الذي حدث، لم يتلقَ x أي قيمة في السطر 7، وبالتالي لم تتغير قيمته؛
  • الأسطر 14-21: نكرر نفس العملية ولكن مع اعتراض استثناء من مستوى أعلى من النوع BaseException. ونظرًا لأن الاستثناء ZeroDivisionError مشتق من الفئة BaseException، فإن الجملة except ستوقفه؛
  • الأسطر 23-36: نضع عدة جمل except للتعامل مع أنواع متعددة من الاستثناءات. سيتم تنفيذ جملة واحدة فقط من نوع except أو لن يتم تنفيذ أي منها إذا لم يتطابق الاستثناء مع أي جملة من جمل «except»؛
  • الأسطر 38-50: نكرر نفس الإجراء مع تغيير ترتيب الجمل [except] لإظهار دور كل منها؛
  • الأسطر 52-58: قد لا تحتوي الجملة except على أي وسيطة. في هذه الحالة، فإنها توقف جميع الاستثناءات؛
  • الأسطر 60-67: تُدرج الاستثناء ValueError؛
  • الأسطر 69-75: يتم استرداد المعلومات التي تحملها الاستثناء؛
  • الأسطر 77-83: تشرح كيفية إثارة (raise) استثناء؛
  • الأسطر 86-106: توضح استخدام فئة استثناء خاصة MyError. تكتفي الفئة MyError بالاشتقاق من الفئة الأساسية BaseException. وهي لا تضيف أي شيء إلى فئتها الأساسية. ولكن الآن، يمكن تسميتها صراحةً في جمل except؛
  • الأسطر 108-130: توضح استخدام جملة finally؛
  • الأسطر 132-139: توضح هذه الأسطر أن الجملة [except] ليست إلزامية؛

نتائج العرض على الشاشة هي كما يلي:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_02.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_02.py", line 136, in <module>
    x = 4 / 0
ZeroDivisionError: division by zero
essai n° 0 : division by zero
x=2
essai n° 1 : division by zero
essai n° 2 : (Exception) division by zero
essai n° 3 : (ZeroDivisionError) division by zero
essai n° 4 : il y a eu un problème
essai n° 5 : invalid literal for int() with base 10: 'x'
essai n° 6 : invalid literal for int() with base 10: 'x', paramètres=("invalid literal for int() with base 10: 'x'",)
essai n° 7 : ('param1', 'param2', 'param3'), paramètres=('param1', 'param2', 'param3')
essai n° 8 : ('info1', 'info2', 'info3'), paramètres=('info1', 'info2', 'info3')
essai n° 9 : mon msg d'erreur
essai n° 10 : finally x=1
essai n° 11 : exception
essai n° 11 : finally x=None
essai n° 12 : finally x=None

Process finished with exit code 1

5.3. البرنامج النصي [exceptions_03]

يوضح هذا البرنامج النصي الجديد كيفية تمرير الاستثناءات عبر سلسلة الدوال المستدعية:


# استثناء خاص
class MyError(BaseException):
    pass


# ثلاث دوال
def f1(x: int) -> int:
    # لا تتم معالجة الاستثناءات - يتم تمريرها تلقائيًا
    return f2(x)


def f2(y: int) -> int:
    # لا تتم معالجة الاستثناءات - يتم تمريرها تلقائيًا
    return f3(y)


def f3(z: int) -> int:
    if (z % 2) == 0:
        # إذا كان z زوجيًا، يتم إلقاء استثناء
        raise MyError("exception dans f3")
    else:
        return 2 * z


# ---------- main

# تنتقل الاستثناءات عبر سلسلة الطرق التي تم استدعاؤها
# حتى تعترضها إحدى الطرق. وهنا ستكون main
try:
    print(f1(4))
except MyError as erreur:
    print(f"type : {type(erreur)}, arguments : {erreur.args}")


# ثلاث دوال أخرى تعمل على إثراء الاستثناءات التي ترسلها
def f4(x: int) -> int:
    try:
        return f5(x)
    except MyError as erreur:
        # يتم إثراء الاستثناء ثم إعادة إطلاقه
        raise MyError("exception dans f4", erreur)


def f5(y: int) -> int:
    try:
        return f6(y)
    except MyError as erreur:
        # نقوم بتوسيع الاستثناء ثم نعيد إطلاقه
        raise MyError("exception dans f5", erreur)


def f6(z: int) -> int:
    if (z % 2) == 0:
        # نطلق استثناءً إذا كان z زوجيًا
        raise MyError("exception dans f6")
    else:
        return 2 * z


# ---------- main
try:
    print(f4(4))
except MyError as erreur:
    # عرض الاستثناء
    print(f"type : {type(erreur)}, arguments : {erreur.args}")
    # يمكن تتبع مكدس الاستثناءات
    err = erreur
    # يتم عرض رسالة الخطأ
    print(err.args[0])
    # هل الاستثناء مغلف؟
    while len(err.args) == 2 and isinstance(err.args[1], BaseException):
        # تغيير الاستثناء
        err = err.args[1]
        # الحجة الأولى هي رسالة الخطأ
        print(err.args[0])

ملاحظات:

  • في الأسطر 25-32، في استدعاء main --> f1 --> f2 --> f3 (السطر 30)، ستنتقل الاستثناء MyError الذي أطلقه f3 إلى main. وستتم معالجتها عندئذٍ بواسطة الجملة except في السطر 31؛
  • الأسطر 61-75: في الاستدعاء main --> f4 --> f5 --> f6 (السطر 62)، ستنتقل الاستثناء MyError الذي أطلقته f6 إلى main. وستتم معالجتها عندئذٍ بواسطة الجملة except في السطر 63. هذه المرة، أثناء صعودها عبر سلسلة الدوال المستدعية، فإن الاستثناء MyError الذي يصعد، يكون هو نفسه مغلفًا في استثناء آخر؛
  • الأسطر 66-75: توضح كيفية الصعود عبر مكدس الاستثناءات؛
  • السطر 71: تُرجع الدالة [isinstance(instance, Classe)] القيمة True إذا كان الكائن [instance] من النوع [Classe] أو مشتقًا منه. هنا، استخدمنا الاستثناء الأعلى مستوى [BaseException]، مما يضمن لنا استرداد جميع الاستثناءات؛

نتائج العرض على الشاشة هي كما يلي:

1
2
3
4
5
6
7
8
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_03.py
type : <class '__main__.MyError'>, arguments : ('exception dans f3',)
type : <class '__main__.MyError'>, arguments : ('exception dans f4', MyError('exception dans f5', MyError('exception dans f6')))
exception dans f4
exception dans f5
exception dans f6

Process finished with exit code 0