درس ۲۴: مدیریت خطا در پایتون: Warning ،raise Exception و Assertion

مدیریت خطا در پایتون:Warning ،raise Exception و Assertion

Photo by Sandy Manoa

این درس در ادامه درس پیش می‌باشد و به شرح مفاهیم باقی‌مانده پیرامون مفهوم Exception در زبان برنامه‌نویسی پایتون می‌پردازد. اینکه چگونه می‌توان با استفاده از دستور raise یک Exception را به صورت عمدی در برنامه بروز داد و همچنین چگونه می‌شود یک Exception در زبان برنامه‌نویسی پایتون ایجاد نماییم. در ادامه این درس به بررسی مفاهیم Warning و Assertion در زبان‌برنامه‌نویسی پایتون و نیز ارتباط آن‌ها با Exception می‌پردازد.

سطح: متوسط



دستور raise

از درس پیش با Exception آشنا شده‌ایم و مشاهده کردیم در زمان اجرای برنامه پایتونی (Runtime) تمامی خطاها در قالب یک Exception اعلام می‌گردند. اما در برنامه‌نویسی زمان‌های بسیاری خواهد بود که برنامه‌نویس می‌بایست خود اقدام به بروز Exception نماید. یک ماژول در هنگام انجام کار مشخصی ممکن است با وضعیت‌های مختلفی روبرو گردد که می‌بایست این وضعیت‌ها را به ماژول سطح بالاتر خود اعلام کند تا در نهایت نتیجه و توضیح مناسب برای کاربر فراهم گردد. برای مثال در پیاده‌سازی API ماژولی که انجام خدمت را به عهده دارد، هنگامی که به خطا یا وضعیتی خاص برخورد می‌کند، می‌تواند این وضعیت را در قالب بروز یک Exception اعلام کند و ماژولی که وظیفه تولید پاسخ یا Response را برعهده دارد، بر اساس نوع Exception رخ داده می‌تواند یک Response مناسب تولید نماید.

در زبان برنامه‌نویسی پایتون از دستور raise [اسناد پایتون] برای بروز یک Exception استفاده می‌گردد:

raise exception_object

به نمونه کد زیر توجه نمایید:

1def self_sum_int(a):
2    if not isinstance(a, int):
3        raise TypeError()
4
5    return a + a
6
7res = self_sum_int('C')
8print(res)
Traceback (most recent call last):
  File "sample.py", line 7, in <module>
    res = self_sum_int('C')
  File "sample.py", line 3, in self_sum_int
    raise TypeError()
TypeError

در نمونه کد بالا ما یک شی از کلاس TypeError ایجاد و آن را raise کردیم (سطر ۳). همانطور که مشاهده می‌کنید، شرح Exception در Traceback (سطر پایانی) با آن چیزی که در درس پیش شاهد آن بودیم، متفاوت است و همچنین علت بروز Exception نیز raise شدن آن اعلام شده است.

می‌توان در هنگام نمونه‌سازی از کلاس Exception مورد نظر، یک متن دلخواه (یک شی از نوع str) به عنوان شرح Exception در زمان نمونه‌سازی به صورت آرگومان ارسال کنیم:

1def self_sum_int(a):
2    if not isinstance(a, int):
3        raise TypeError(f"The input must be 'int' type, {a!r} is {type(a)}")
4
5    return a + a
6
7res = self_sum_int('C')
8print(res)
Traceback (most recent call last):
  File "sample.py", line 7, in <module>
    res = self_sum_int('C')
  File "sample.py", line 3, in self_sum_int
    raise TypeError(f'The input must be of the integer type, {a} is {type(a)}')
TypeError: The input must be 'int' type, 'C' is <class 'str'>

طی درس پیش مشاهده کردیم، چنانچه در زمان handle کردن یک Exception، یک Exception دیگر رخ دهد؛ در نتیجه Traceback نهایی نیز شامل یک Traceback به ازای هر Exception خواهد بود. این امکان نیز توسط دستور raise برای برنامه‌نویس فراهم می‌باشد. می‌توان با استفاده از دستور from در کنار raise، دو Exception را به یکدیگر متصل و سپس raise کرد:

raise exception_object from other_exception_object

به نمونه کد ساده زیر و خروجی آن توجه نمایید:

1def sum_int(a, b):
2    try:
3        return a + b
4    except Exception as exception:
5        raise RuntimeError("Something bad happened") from exception
6
7res = sum_int(3, 'C')
8print(res)
Traceback (most recent call last):
  File "sample.py", line 3, in sum_int
    return a + b
TypeError: unsupported operand type(s) for +: 'int' and 'str'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "sample.py", line 7, in <module>
    res = sum_int(3, 'C')
  File "sample.py", line 5, in sum_int
    raise RuntimeError("Something bad happened") from exception
RuntimeError: Something bad happened

به عنوان یک نمونه کاربرد، از این روش می‌توان برای ایجاد یک Wrapper برای چندین Exception بهره برد. در این حالت کد سطح بالاتر تنها نیاز است یک نوع Exception را handle نماید:

 1def sum_int(a, b):
 2    try:
 3        return a + b
 4    except TypeError as type_err:
 5        raise RuntimeError(f'Something bad happened \n    => {str(type_err)}') from type_err
 6
 7
 8
 9try:
10    res = sum_int(3, 'C')
11    print(res)
12
13except RuntimeError as runtime_err:
14    print(f'{runtime_err.__class__.__name__}: {str(runtime_err)}')
RuntimeError: Something bad happened
    => unsupported operand type(s) for +: 'int' and 'str'

ایجاد Exception

در زبان برنامه‌نویسی پایتون با ایجاد یک کلاس و ارث‌بری از Exception یا یکی از subclassهای آن می‌توان یک Exception جدید ایجاد نمود:

 1class NegativeNumberError(Exception):
 2    """Raised when the input value is negative number"""
 3    pass
 4
 5
 6def plus(num):
 7    if num < 0:
 8        raise NegativeNumberError(f'{num} is a negative number!')
 9
10    return num + num
11
12
13try:
14    print(plus(10))
15    print('*' * 30)
16    print(plus(-5))
17
18except NegativeNumberError as err:
19    print(str(err))
20
21except:
22    print('Something bad happened!')
20
******************************
-5 is a negative number!

بدیهی است که می‌توان کلاس‌های Exception خود را مطابق با میل خود پیاده‌سازی نمود:

 1class NegativeNumberError(Exception):
 2    """Raised when the input value is negative number"""
 3
 4    def __init__(self, number, message="Number must be positive"):
 5        self.number = number
 6        self.message = message
 7        super().__init__(self.message)
 8
 9    def __str__(self):
10        return f'ERROR[{self.number}] -> {self.message}'
11
12
13def plus(num):
14    if num < 0:
15        raise NegativeNumberError(num)
16
17    return num + num
18
19
20try:
21    print(plus(10))
22    print('*' * 30)
23    print(plus(-5))
24
25except NegativeNumberError as err:
26    print(str(err))
27
28except:
29    print('Something bad happened!')
20
******************************
ERROR[-5] -> Number must be positive

توجه

در زبان‌برنامه‌نویسی پایتون پیشنهاد می‌شود که اگر هدف از ایجاد Exception نمایش یک خطا باشد، در انتهای نام کلاس از واژه Error استفاده گردد.

ماژول warnings

تا این لحظه با مفهوم Exception آشنا شده‌ایم. می‌دانیم که بروز Exception در واقع اعلام یک خطا یا یک رویداد مهم در برنامه می‌باشد که می‌بایست حتما handle شود، در غیر این صورت برنامه قادر به انجام دستورات نخواهد بود.

اما گاهی اعلام یک رویداد آنقدر مهم نیست که بخواهد روند اجرای برنامه را تهدید کند. بلکه صرفا یک هشدار برای توجه بیشتر یا اصلاح رفتار برای نسخه‌های بعدی خواهد بود که بیشتر کاربرد آن برای توسعه‌دهندگان برنامه می‌باشد تا کاربرانی که به نوعی مصرف‌کنندگان آن برنامه محسوب می‌شوند. در زبان برنامه‌نویسی پایتون، ماژول warnings [اسناد پایتون] برای استفاده در چنین زمان‌هایی فراهم آورده شده است [PEP 230].

پیش از مراجعه به این ماژول از کتابخانه استاندارد زبان برنامه‌نویسی پایتون لازم است نگاهی دوباره به انتهای فهرست سلسله‌مراتب وراثت Exceptionها که در درس پیش آن را بررسی کردیم بیاندازیم، در انتهای این فهرست کلاس‌هایی با پسوند Warning قراردارند [Exception hierarchy]:

Exception Hierarchy در پایتون به همراه warnings

همان‌طور که مشاهده می‌شود، تمام این کلاس‌ها از کلاسی با نام Warning ارث‌بری دارند که خود این کلاس نیز از کلاس Exception ارث‌بری دارد.

این‌ها Warning هستند، Exceptionهایی که هدف از توسعه آنها اعلام یک هشدار می‌باشد و نه اعتراضی که تنبیه آن توقف برنامه باشد. با این حال به نمونه کد زیر توجه نمایید:

1def sum_int(a, b):
2    raise DeprecationWarning('"sum_int" will be removed in version 2.0')
3    sum = a + b
4    print(sum)
5
6
7sum_int(6, 5)
8print('Done.')
Traceback (most recent call last):
  File "sample.py", line 7, in <module>
    sum_int(6, 5)
  File "sample.py", line 2, in sum_int
    raise DeprecationWarning('"sum_int" will be removed in version 2.0')
DeprecationWarning: "sum_int" will be removed in version 2.0

ساختار سلسله‌مراتب به ما گفته بود که Warningها در اصل Exception هستند (وجود رابطه IS-A به دلیل وراثت - درس هجدهم) و زمانی که یک Exception به اصطلاح raise شود، حتما می‌بایست یک handler برای آن پیش‌بینی شده باشد. در واقع اگر برای بروز یک Warning از دستور raise استفاده شود، دستور raise همان کاری را با Warning انجام می‌دهد که با هر نوع Exception دیگری انجام خواهد داد.

تابع warnings.warn

اگر بخواهیم بروز یک Exception به صورتی هشدارگونه باشد، می‌بایست به سراغ ماژول warnings برویم. اکنون اگر نمونه کد قبل را با کمک این ماژول بازنویسی نماییم، نتیجه زیر حاصل می‌گردد:

 1import warnings
 2
 3def sum_int(a, b):
 4    warnings.warn('"sum_int" will be removed in version 2.0', DeprecationWarning)
 5    sum = a + b
 6    print(sum)
 7
 8
 9sum_int(6, 5)
10print('Done.')
sample.py:4: DeprecationWarning: "sum_int" will be removed in version 2.0
  warnings.warn('"sum_int" will be removed in version 2.0', DeprecationWarning)
11
Done.

همان‌طور که مشاهده می‌شود تنها یک پیام هشدار در خروجی چاپ (print) می‌شود و دیگر خبری از Traceback نیست و برنامه بدون هیچ اخلالی باموفقیت تا خط پایان به اجرای خود ادامه داده است.

برای اعلام یک هشدار از تابع warn در ماژول warnings استفاده می‌شود [اسناد پایتون] که تعریف آن به شکل زیر می‌باشد:

warn(message, category=None, stacklevel=1, source=None)

بر اساس تعریف، این تابع یک پارامتر اجباری (message) و سه پارامتر اختیاری دارد.

  • message: می‌بایست یک شی str باشد و متن هشداری است که می‌خواهیم نمایش داده شود.

  • category: نوع Warning را مشخص می‌کند که می‌بایست نام یک subclass از کلاس Warning باشد. برای مشاهده انواع Warningهای از پیش آماده در پایتون می‌توانید به Warning Categories مراجعه نمایید. ارسال آرگومان برای این پارامتر اختیاری است و در صورت عدم ارسال، نوع UserWarning به صورت پیش‌فرض در نظر گرفته خواهد شد.

  • stacklevel: اگر دقت کرده باشید متن هشدار شامل اطلاعاتی از محل بروز آن می‌باشد. این پارامتر یک عدد از نوع int و بزرگتر یا مساوی از 1 را دریافت و تعیین می‌کند که این اطلاعات مربوط به کدام سطح از فراخوانی کد و رسیدن به این هشدار باشد. به این صورت که: عدد 1 (مقدار پیش‌فرض) به محل دقیق بروز هشدار، عدد 2 به یک مرحله قبل‌تر از محل بروز هشدار و ...

     1import warnings
     2
     3def sum_int(a, b):
     4    print('-' * 30,  'stacklevel=1')
     5    warnings.warn('"sum_int" will be removed in version 2.0', stacklevel=1)
     6    print('-' * 30,  'stacklevel=2')
     7    warnings.warn('"sum_int" will be removed in version 2.0', stacklevel=2)
     8    print('-' * 30,  'stacklevel=3')
     9    warnings.warn('"sum_int" will be removed in version 2.0', stacklevel=3)
    10    print('-' * 30,  'stacklevel=4')
    11    warnings.warn('"sum_int" will be removed in version 2.0', stacklevel=4)
    12    print('-' * 30,  'stacklevel=5')
    13    warnings.warn('"sum_int" will be removed in version 2.0', stacklevel=5)
    14    print('-' * 30)
    15    sum = a + b
    16    print(sum)
    17
    18
    19def action(a, b):
    20   sum_int(6, 5)
    21
    22
    23action(6, 5)
    24print('Done.')
    
    ------------------------------ stacklevel=1
    sample.py:5: UserWarning: "sum_int" will be removed in version 2.0
      warnings.warn('"sum_int" will be removed in version 2.0', stacklevel=1)
    ------------------------------ stacklevel=2
    sample.py:20: UserWarning: "sum_int" will be removed in version 2.0
      sum_int(6, 5)
    ------------------------------ stacklevel=3
    sample.py:23: UserWarning: "sum_int" will be removed in version 2.0
      action(6, 5)
    ------------------------------ stacklevel=4
    sys:1: UserWarning: "sum_int" will be removed in version 2.0
    ------------------------------ stacklevel=5
    ------------------------------
    11
    Done.
    

Warnings Filter

حالتی را تصور کنید که برنامه شما پر از Warningهای متنوع می‌باشد. Warnings Filter امکانی است برای اینکه مشخص کنیم کدام نوع Warning نادیده گرفته شود یا کدام نوع نمایش داده شود یا کدام نوع همچون یک Exception واقعی رفتار کند. برای این منظور از از تابع simplefilter در ماژول warnings استفاده می‌شود [اسناد پایتون] که تعریف آن به شکل زیر می‌باشد:

simplefilter(action, category=Warning, lineno=0, append=False)

بر اساس تعریف، این تابع یک پارامتر اجباری (action) و سه پارامتر اختیاری دارد.

  • action: از نوع str بوده و می‌تواند یکی از مقادیر پایین باشد. این مشخص می‌کند که چه عملیاتی می‌بایست بر روی Warningها اعمال شود:

    مقدار

    توضیحات

    'default'

    حالت پیش‌فرض، هر Warning به ازای سطری که در آن قرار دارد یکبار چاپ شود

    'error'

    تبدیل رفتار Warning به Exception واقعی - بروز خطا

    'ignore'

    نادیده گرفتن Warning

    'always'

    Warning هر بار چاپ شود

    'module'

    هر Warning به ازای هر ماژول تنها یکبار چاپ شود

    'once'

    هر Warning به ازای کل برنامه تنها یکبار چاپ شود

  • category: نوع Warning را مشخص می‌کند که می‌بایست نام یک subclass از کلاس Warning باشد. ارسال آرگومان برای این پارامتر اختیاری است و در صورت عدم ارسال، عمل مشخص شده توسط پارامتر action برای تمام انواع Warningها در برنامه اعمال می‌گردد.

به نمونه کدهای زیر توجه نمایید:

1import warnings
2warnings.simplefilter('ignore')
3# $ python3 -Wi sample.py
4# $ python3 -Wignore sample.py
5
6
7print('-------Before #01-------')
8warnings.warn('#01')
9print('-------After  #01-------')
-------Before #01-------
-------After  #01-------
 1import warnings
 2warnings.simplefilter('ignore', DeprecationWarning)
 3# $ python3 -Wignore::DeprecationWarning sample.py
 4# $ python3 -Wi::DeprecationWarning sample.py
 5
 6
 7print('-------Before #02-------')
 8warnings.warn('#02')
 9print('-------After  #02-------')
10
11print('-------Before #03-------')
12warnings.warn('#03', DeprecationWarning)
13print('-------After  #03-------')
-------Before #02-------
sample.py:8: UserWarning: #02
  warnings.warn('#02')
-------After  #02-------
-------Before #03-------
-------After  #03-------
1import warnings
2warnings.simplefilter('error')
3# $ python3 -We sample.py
4# $ python3 -Werror sample.py
5
6
7print('-------Before #04-------')
8warnings.warn('#04')
9print('-------After  #04-------')
-------Before #04-------
Traceback (most recent call last):
  File "sample.py", line 8, in <module>
    warnings.warn('#04')
UserWarning: #04

نکته

اعمال Filter در زمان اجرای اسکریپت نیز با استفاده از کلید W- ممکن می‌باشد [اسناد پایتون] که در هر نمونه کد، معادل دستور اجرای پایتون نیز به صورت کامنت درج شده است.

نکته

همانند Exceptionها می‌توانید انواع Warning خود را ایجاد نمایید. برای این منظور تنها کافی است یک کلاس ایجاد نمایید که از کلاس Warning یا یکی از subclassهای آن ارث‌بری داشته باشد.

توجه

این بخش به دلیل وابستگی مبحث Warning با مبحث مهم Exception در زبان‌برنامه‌نویسی پایتون و صرفا به منظور آشنایی خوانندگان با همچنین قابلیتی در این زبان تهیه شده است. ماژول warnings امکانات گسترده‌تری را فراهم می‌آورد که پرداختن به تمام آن‌ها خارج از حوصله این درس می‌بود، بنابراین علاقه‌مندان برای مطالعه بیشتر می‌توانند به مستندات رسمی پایتون مراجعه نمایند.

دستور assert

ادعا یا Assertion در برنامه‌نویسی به عبارت‌های ساده از شرط‌های بولی گفته می‌شود که درستی یک «وضعیت» یا یک «حقیقت» در کد را بررسی می‌کنند. باید توجه داشت Assertion در واقع یک ابزار برای کمک به توسعه برنامه می‌باشد که کاربرد آن در ایجاد تست کد در زمان تست‌نویسی و دیباگ (Debug) برنامه در محیط توسعه می‌باشد و نه در محیط اجرای برنامه به عنوان محصول.

فلوچارت یک Assertion به صورت زیر ترسیم می‌شود:

دستور ``assert`` در پایتون - فلوچارت Assertion

در زبان برنامه‌نویسی پایتون Assertion با استفاده از دستور assert پیاده‌سازی می‌گردد [اسناد پایتون] و با یکی از دو سینتکس زیر قابل پیاده‌سازی می‌باشد:

assert condition_expression
assert condition_expression, 'error_message'

به نمونه کد زیر توجه نمایید:

 1def average(numbers):
 2    assert len(numbers) != 0
 3    return sum(numbers)/len(numbers)
 4
 5numbers = [1, 2, 3, 4, 5]
 6print(f'Average of {numbers}: {average(numbers)}')
 7
 8print('-' * 30)
 9
10numbers = []
11print(f'Average of {numbers}: {average(numbers)}')
Average of [1, 2, 3, 4, 5]: 3.0
------------------------------
Traceback (most recent call last):
  File "sample.py", line 11, in <module>
    print(f'Average of {numbers}: {average(numbers)}')
  File "sample.py", line 2, in average
    assert len(numbers) != 0
AssertionError

فرض توسعه‌دهنده تابع average مثال قبل این بوده که به این تابع داده‌ای با طول صفر ارسال نمی‌گردد، ولی اگر در زمان تست یا ادامه مراحل توسعه برنامه این مقدار ارسال گردد، باید یک فکری برای اصلاح آن کرد! چرا که این تابع آمادگی کافی برای تبدیل شدن به یک باگ در برنامه را دارد!

می‌توان برای دستور assert یک پیام خطا نیز اختصاص داد:

1def average(numbers):
2    assert len(numbers) != 0, 'List[numbers] is empty.'
3    return sum(numbers)/len(numbers)
4
5numbers = []
6print(f'Average of {numbers}: {average(numbers)}')
Traceback (most recent call last):
  File "sample.py", line 6, in <module>
    print(f'Average of {numbers}: {average(numbers)}')
  File "sample.py", line 2, in average
    assert len(numbers) != 0, 'List[numbers] is empty.'
AssertionError: List[numbers] is empty.

همانطور پیش‌تر بیان شده دستورهای assert یک قابلیت برای زمان توسعه می‌باشند بنابراین باید توجه داشت که تمامی این دستورات هنگامی که برنامه با کلید بهینه‌سازی (Optimization - درس چهارم) یعنی O- یا OO- اجرا گردد، در زمان کامپیال حذف و در بایت‌کد قرار نخواهند گرفت:

$ python -O script.py

این شرایط مشابه حالتی است که بجای دستور assert، مستقیم از دستور raise به شکل زیر استفاده نماییم:

if __debug__:
    if not condition_expression: raise AssertionError()
if __debug__:
    if not condition_expression: raise AssertionError('error_message')

__debug__ یک متغیر داخلی در محیط اجرای پایتون با مقدار پیش‌فرض True می‌باشد [اسناد پایتون]. مقدار این متغیر در تمام طول مدت اجرای برنامه ثابت خواهد بود و تنها زمانی که برنامه با کلید بهینه‌سازی اجرا گردد، مقدار آن به False تغییر می‌یابد. بنابراین دستور raise AssertionError هیچگاه اجرا نخواهد شد.

همچنین نباید فراموش کرد که AssertionError یکی از Exceptionهای آماده پایتون می‌باشد [اسناد پایتون]. بنابراین هنگام بروز ممکن است توسط دستور try، به صورت ناخواسته handle شود:

 1def average(numbers):
 2    assert len(numbers) != 0, 'List[numbers] is empty.'
 3    return sum(numbers)/len(numbers)
 4
 5try:
 6
 7    numbers = []
 8    print(f'Average of {numbers}: {average(numbers)}')
 9
10except Exception as err: # or except:
11    print('Something bad happened!')
Something bad happened!


😊 امیدوارم مفید بوده باشه