به‌کارگیری استثناها برای پیش‌گیری از از کار افتادن برنامه

مدیریّت خطاها به ویژه زمانی مهم است که برنامه پس از رخ دادن خطا کارِ زیادی برای انجام دادن داشته باشد. این وضع اغلب در برنامه‌هایی رخ می‌دهد که از کاربر ورودی می‌گیرند. اگر برنامه به ورودی‌های نامناسب به درستی پاسخ دهد، به جای از کار افتادن می‌تواند به ورودی‌های درستِ بیشتری رسیدگی کند.

بیایید ماشین حساب ساده‌ای بسازیم که تنها عمل تقسیم را انجام می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")

while True:
    first_number = input("\nFirst number: ")
    if first_number == 'q':
        break
    second_number = input("Second number: ")
    if second_number == 'q':
        break
    answer = int(first_number) / int(second_number)
    print(answer)

این برنامه از کاربر می‌خواهد تا first_number را وارد کند (۱) و اگر برای خروج q را فشار نداد، second_number را هم وارد کند (۲). سپس این دو عدد را بر هم تقسیم می‌کنیم تا answer به دست بیاید (۳). این برنامه هیچ کاری برای مدیریّت خطا انجام نمی‌دهد بنابراین درخواست تقسیم بر صفر موجب از کار افتادن برنامه می‌شود:

1
2
3
4
5
6
7
8
9
Give me two numbers, and I'll divide them.
Enter 'q' to quit.

First number: 5
Second number: 0
Traceback (most recent call last):
  File "division_calculator.py", line 9, in <module>
    answer = int(first_number) / int(second_number)
ZeroDivisionError: division by zero

این که برنامه از کار افتاده بد است؛ و علاوه بر آن نشان دادنِ traceback به کاربر هم فکر خوبی نیست. زیرا کاربران غیر فنّی را گیج می‌کند و در صورتی که کسی به دنبال کارهای مخرّب باشد، به کمکِ آن ممکن است بیش از آن چیزی که شما بخواهید در مورد برنامه‌یتان اطّلاعات کسب کند. مثلاً نام برنامه‌ی شما را می‌فهمد یا می‌بیند که کجای برنامه‌ی شما به درستی کار نمی‌کند. یک مهاجم کارآزموده گاهی می‌تواند از این دانسته‌ها استفاده کند تا نوعِ حمله علیهِ کد شما را تعیین کند.

بلوک else

می‌توانیم با قرار دادن خطّی که ممکن است ایجاد خطا کند داخل یک بلوک try این برنامه را در برابر خطا مقاوم‌تر کنیم. خطا در خطّی رخ می‌دهد که تقسیم در آن انجام می‌گیرد بنابراین آن‌جا جایی است که در بلوک try-except قرار می‌دهیم. این مثال همچنین شامل یک بلوک else است. هر کدی که اجرای آن به اجرای موفّق بلوک try وابسته است در بلوک else قرار می‌گیرد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
--snip--

while True:
    --snip--

    if second_number == 'q':
        break

    try:
        answer = int(first_number) / int(second_number)
    except ZeroDivisionError:
        print("You can't divide by 0!")
    else:
        print(answer)

از پایتون می‌خواهیم که تلاش خود را برای به انجام رساندن عملیّات تقسیم در یک بلوک try بکند (۱). این بلوک تنها شامل کدی است که ممکن است موجب بروز خطا شود. هر کدی که به اجرای موفقیّت‌آمیز بلوک try وابسته باشد به بلوک else افزوده می‌شود. در این مورد اگر عملیّات تقسیم موفّق باشد از بلوک else برای چاپ کردن نتیجه در ۳ استفاده می‌کنیم.

بلوک except به پایتون می‌گوید که هنگام رخ دادن ZeroDivisionError چه پاسخی بدهد (۲). اگر بلوک try به دلیل خطای تقسیم بر صفر با موفقیّت انجام نشد، پیامی دوستانه چاپ می‌کنیم که به کاربر بگوید چگونه از این خطا پرهیز کند. اجرای برنامه ادامه می‌یابد و کاربر هرگز traceback‌ای نمی‌بیند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Give me two numbers, and I'll divide them.
Enter 'q' to quit.

First number: 5
Second number: 0
You can't divide by 0!

First number: 5
Second number: 2
2.5

First number: q

بلوک try-except-else این گونه کار می‌کند: پایتون تلاش می‌کند که کد داخل بلوک try را اجرا کند. تنها کدی باید در این بلوک قرار بگیرد که ممکن است موجب بروز استثنا شود. گاهی شما کد دیگری دارید که باید تنها در صورت موفقیّت‌آمیز بودن اجرای بلوک try اجرا شود؛ این کد در بلوک else قرار می‌گیرد. بلوک except به پایتون می‌گوید که در صورت وقوع استثنای خاصّی در اجرای کد بلوک try، چه کاری انجام دهد.

با پیش‌بینی منابع احتمالی خطا می‌توانید برنامه‌های مقاومی بنویسید که حتّی در صورت برخوردن به داده‌های نامعتبر یا پیدا نشدنِ منابع، به کار خود ادامه دهند. کد شما در برابر خطاهای غیرعمدی کاربران یا حمله‌های مخرّب مقاومت خواهد کرد.