Python حرفه‌ای: مدیریت خطا در پایتون

Professional Python: Error management in Python

19 اسفند 1399
درسنامه درس 18 از سری پایتون حرفه‌ای
Python حرفه ای: مدیریت خطا در پایتون (قسمت 18)

ماهیت خطاها در زبان پایتون

یکی از جنبه های جدایی ناپذیر برنامه نویسی بروز خطا در کدها است و هر چقدر هم که برنامه نویس با تجربه و متخصصی باشید باز هم به نوعی به خطاها برخورد خواهید کرد. مسئله اینجاست که ماهیت این خطاها در زبان پایتون چیست؟ به کد زیر توجه کنید:

print(1 + 'hello')

ما در اینجا یک عدد را با یک رشته جمع زده ایم و طبیعتا چنین کدی اشتباه است. با اجرای این کد نتیجه زیر را دریافت می کنیم:

Traceback (most recent call last):

  File "/home/amir/Desktop/Roxo Academy/Python/test.py", line 1, in <module>

    print(1 + 'hello')

TypeError: unsupported operand type(s) for +: 'int' and 'str'

همانطور که می بینید این خطا به ما می گوید اجازه جمع زدن int (عدد صحیح) و str (رشته) را نداریم. اگر از نظر منطقی به این مسئله فکر کنید نیز به همین نتیجه می رسید؛ از نظر ریاضی امکان ندارد که یک رشته با یک عدد جمع زده شود. به این دسته از خطاها که باعث توقف کامل برنامه می شوند exception می گوییم. این خطاها آنچنان مهم هستند که مفسر پایتون در مواجهه با آها فرآیند اجرای برنامه را متوقف کرده و خطایی را به شما نمایش می دهد. برای اثبات این موضوع می توانیم از کد زیر استفاده کنیم:

print("BEFORE print")




print(1 + 'hello')




print("AFTER print")

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

BEFORE print

Traceback (most recent call last):

  File "/home/amir/Desktop/Roxo Academy/Python/test.py", line 3, in <module>

    print(1 + 'hello')

TypeError: unsupported operand type(s) for +: 'int' and 'str'

همانطور که می بینید دستور print اول اجرا شده است و عبارت BEFORE print را دریافت کرده ایم اما خبری از print دوم نیست. چرا؟ به دلیل اینکه به محض رسیدن به خطا، اجرای کدها متوقف می شود. حتما شما هم متوجه شده اید که این مسئله به شدت برای برنامه ما مضر است. ما در حال حاضر در حال تست چند خط کد ساده هستیم اما این خطاها در یک برنامه واقعی باعث خراب شدن برنامه می شود. به طور مثال اگر یک وب سایت داشته باشیم، وب سایت ما از دسترسی خارج شده و باعث انواع و اقسام مشکلات دیگر می شود.

انواع خطا در زبان پایتون

خطاها در زبان پایتون انواع مختلفی دارند بنابراین بهتر است با چند مورد رایج از این خطاها آشنا شویم:

https://docs.python.org/3/library/exceptions.html#concrete-exceptions

با مراجعه به لینک بالا می توانید انواع این خطاها را مشاهده کرده و توضیحات آن را مطالعه بفرمایید. به طور مثال یکی از این خطاها خطای NameError است و در توضیحات آن آمده است که:

Raised when a local or global name is not found. This applies only to unqualified names. The associated value is an error message that includes the name that could not be found.

ترجمه: این خطا زمانی پرتاب می شود که نامی سراسری یا محلی پیدا نشود. این مسئله تنها برای نام های unqualified صادق است. مقدار این exception یک پیام خطا است که شامل «نام پیدا نشده» می شود.

unqualified name به معنی نام غیر مستقیم و غیر صریح است. به طور مثال نام java.util.ArrayList یک نام qualified است (یعنی آدرس کامل ArrayList را نیز می بینیم) در صورتی که ArrayList یک نام unqualified است. به مثال زیر توجه کنید:

print(f'my name is {name}')

با اجرای کد بالا خطای زیر را می گیریم:

Traceback (most recent call last):

  File "/home/amir/Desktop/Roxo Academy/Python/test.py", line 1, in <module>

    print(f'my name is {name}')

NameError: name 'name' is not defined

همانطور که از خطای بالا مشخص است یک NameError را دریافت کرده ایم. چرا؟ به دلیل اینکه هیچ متغیری به نام name در کد ما وجود ندارد.

یک نوع خطای مشهور دیگر، خطای SyntaxError است. syntax به معنی «نحو» یا همان گرامر کدها است (دستور العمل های روش نوشتن کد) و زمانی اتفاق می افتد که از قوانین ساختاری و نوشتاری پایتون پیروی نکنید. به عنوان مثال به کد زیر توجه کنید:

def my_function()

    print("THIS IS A FUNCTION")

آیا مشکلی در این کد می بینید؟ من علامت دو نقطه بعد از نام تابع را قرار نداده ام بنابراین یک خطای syntax داریم. با اجرای این کد خطای زیر را دریافت می کنیم:

  File "/home/amir/Desktop/Roxo Academy/Python/test.py", line 1

    def my_function()

                    ^

SyntaxError: invalid syntax

همانطور که می بینید مفسر پایتون یک خطای syntaxError را به من داده است و با علامت ^ دقیقا محل ایجاد خطا را نیز نشانه گذاری کرده است تا بتوانیم آن را عیب یابی کنیم.

خطای رایج بعدی IndexError است و زمانی رخ می دهد که قصد دسترسی به ایندکسی ناموجود را داشته باشیم. به مثال زیر توجه کنید:

def hello():

    li = [1, 2, 3, 4]

    li[5]







hello()

با اجرای این کد خطای زیر را دریافت می کنید:

Traceback (most recent call last):

  File "/home/amir/Desktop/Roxo Academy/Python/test.py", line 6, in <module>

    hello()

  File "/home/amir/Desktop/Roxo Academy/Python/test.py", line 3, in hello

    li[5]

IndexError: list index out of range

همانطور که مشخص است خطای ما از نوع IndexError می باشد. چرا این خطا اتفاق افتاده است؟ به دلیل اینکه ما درخواست دریافت ایندکس ۵ را گرفته ایم اما چنین ایندکسی وجود ندارد. لیست li ۴ عنصر دارید که ایندکس هایشان از صفر تا ۳ می باشند. در ضمن در نظر داشته باشید که اگر به جای لیست ها از دیکشنری ها استفاده کنید دیگر خطای IndexError را نخواهیم داشت بلکه خطای KeyError را می گیریم چرا که دیکشنری ها index ندارند بلکه key دارند اما در عمل معنی هر دو خطا یکی است.

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

ما در این قسمت داده ها را درون کدهای خودمان داشتیم اما در برنامه های واقعی چنین اتفاقی نمی افتد. در برنامه های واقعی اکثر داده ها از خارج از برنامه می آیند؛ به طور مثال در یک وب سایت، فرم ثبت نام توسط کاربر پر می شود و او می تواند هر داده ای را برای ما بفرستد یا در یک بازی آنلاین، حرکات و اعمال کاراکتر ها توسط کاربران کنترل می شود و الی آخر. با این حساب هیچ گاه نباید به داده ها اعتماد کنیم.

مدیریت خطا (error handling)

مدیریت خطا در تمام زبان های برنامه نویسی وجود دارد و به ما اجازه می دهد در صورت برخورد با خطا، برنامه خود را در حال اجرا نگه داشته و از متوقف شدن کامل آن جلوگیری کنیم. در این بخش می خواهم یک روش مدیریت خطا را به شما نشان بدهم. فرض کنید بخواهیم سن کاربر را دریافت کرده و سپس نمایش بدهیم:

age = input('what is your age?')




print(age)

با اجرای این کد یک سوال از ما پرسیده می شود (what is your age) و اگر مقداری را وارد کنیم،‌ آن مقدار برایمان چاپ می شود. کدها بسیار ساده است اما اگر کاربر به جای عدد، رشته ای بی معنی مانند askdjaskldjalshfas را وارد کرده باشد چطور؟ در این حالت عینا رشته askdjaskldjalshfas را دریافت می کنیم. ما شاید بخواهیم با استفاده از شرط if سن کاربر را بررسی کنیم و در این حالت نمی توانیم کاری انجام بدهیم،‌ چه بسا که پروژه به خطا برخورد کند. یک راه حل ساده این است که مقدار age را از همان ابتدا به یک عدد تبدیل کنیم:

age = int(input('what is your age?'))




print(age)

با اجرای این کد مطمئن می شویم که داده کاربر به int (عدد صحیح) تبدیل می شود اما آیا این کافی است؟ اگر کاربر به جای عدد یک رشته مانند asdf وارد کند چطور؟ طبیعتا رشته ها نمی توانند به عدد تبدیل شوند بنابراین به خطای زیر برخورد می کنیم:

what is your age?asdf

Traceback (most recent call last):

  File "/home/amir/Desktop/Roxo Academy/Python/test.py", line 1, in <module>

    age = int(input('what is your age?'))

ValueError: invalid literal for int() with base 10: 'asdf'

چنین کدی اجرای تمام برنامه ما را مختل می کند. ما می توانیم از دستور try ... except استفاده کنیم تا از توقف اسکریپت جلوگیری کنیم:

try:

    age = int(input('what is your age?'))

    print(age)

except:

    print('You should enter a number')




print("LOG")

ما یک بلوک try را نوشته و سپس کدهای خودمان را درون آن قرار می دهیم. اگر کدهای درون trye به هر دلیلی با خطا روبرو شوند به جای دریافت خطا و توقف دفعه ای برنامه، وارد except می شویم و می توانیم خطا را بررسی کنیم. من در این قسمت فقط عبارت You should enter a number را چاپ کرده ام که می گوید «شما باید یک عدد وارد کنید». همچنین برای اثبات متوقف نشدن برنامه، دستور LOG را پس از این کدها چاپ کره ام. حالا با اجرای این کد و ارسال جواب اشتباه، خطا نمیگیریم بلکه You should enter a number را دریافت می کنیم:

what is your age?ROXO

You should enter a number

LOG

بنابراین try ... except از توقف برنامه جلوگیری می کند اما زمانی که به LOG برسیم به انتهای کدهای این فایل رسیده ایم بنابراین برنامه هیچ شانس دوباره ای برای کاربر وجود ندارد تا در صورت اشتباه بتواند آن را تصحیح کند. در چنین حالتی می توانیم از یک حلقه استفاده کنیم:

while True:

    try:

        age = int(input('what is your age?'))

        print(age)

    except:

        print('You should enter a number')

    else:

        print("Thank you")

        break




print("LOG")

ما کدهای خودمان را درون یک حلقه while قرار داده ایم تا مرتبا تکرار شوند اما باید به فکر راهی برای خروج از این حلقه نیز باشیم. اینجاست که بلوک else می تواند به ما کمک کند. بلوک else بخش سومِ try ... except است و زمانی وارد آن می شویم که خطایی وجود نداشته باشد. کلمه try به معنی «امتحان کردن»، کلمه except به معنی «به جز» و else به معنی «در غیر این صورت» است.  با این حساب متوجه می شویم که کد بالا می گوید: کدهای درون try را امتحان کنید، در صورتی که خطایی رخ داد وارد قسمت except و در غیر این صورت (در صورت موفقیت آمیز بودن کد) وارد قسمت else شوید. از این به بعد کد ما آنقدر اجرا خواهد شد تا کاربر عدد صحیحی وارد کند.

سوال بعدی اینجاست که اگر بخواهیم به جای چاپ سن کاربر آن را در عملیاتی به کار ببریم چطور؟ مثال:

while True:

    try:

        age = int(input('what is your age?'))

        10/age

    except:

        print('You should enter a number')

    else:

        print("Thank you")

        break




print("LOG")

ما در این کد به جای چاپ سن کاربر، آن عدد ۱۰ را بر آن تقسیم می کنیم. مشکل اینجاست که اگر کاربر در چنین حالتی عدد صفر را وارد کند به کاربر گرفته می شود: You should enter a number که یعنی «شما باید یک عدد را وارد کنید». این در حالی است که صفر یک عدد است! آیا متوجه مشکل شدید؟ در ریاضی امکان تقسیم هیچ عددی بر صفر نیست و در برنامه نویسی نیز با تقسیم بر صفر خطا دریافت می کنید. طبیعتا با دریافت خطا وارد except شده و دستور print ما اجرا می شود و این مسئله تا انتها ادامه دارد. به زبان ساده تر مشکل اصلی ما این است که هر نوع خطایی وارد قسمت except می شود اما ما می خواهیم بر اساس نوع خطا رفتار خاصی را نشان بدهیم. برای حل این مشکل می توانیم نوع خطا را به قسمت except اضافه کنیم:

while True:

    try:

        age = int(input('what is your age?'))

        10/age

    except ValueError:

        print('You should enter a number')

    else:

        print("Thank you")

        break




print("LOG")

با اضافه کردن ValueError به پایتون گفته ایم که این قسمت برای زمانی است که خطای ما از نوع ValueError باشد (خطا مربوط به مقدار ارسال شده و نوع آن باشد). حالا تصور می کنید با اجرای کد بالا و ارسال عدد صفر چه اتفاقی می افتد؟

what is your age?0

Traceback (most recent call last):

  File "/home/amir/Desktop/Roxo Academy/Python/test.py", line 4, in <module>

    10/age

ZeroDivisionError: division by zero

همانطور که می بینید مثل قبل خطا دریافت کرده ایم و اجرای کدهایمان متوقف شده است بنابراین متوجه می شویم که این دسته از خطاها در برنامه ما مدیریت نشده هستند. خطایی که در این قسمت گرفته ایم ZeroDivisionError (خطای تقسیم بر صفر) است بنابراین باید یک دستور except دیگر را برای آن نیز اجرا کنیم:

while True:

    try:

        age = int(input('what is your age?'))

        10/age

    except ValueError:

        print('You should enter a number')

    except ZeroDivisionError:

        print('You cannot enter zero')

    else:

        print("Thank you")

        break




print("LOG")

با این حساب در صورتی که خطا از نوع ValueError باشد وارد بلوک دوم و اگر از نوع ZeroDivisionError باشد وارد بلوک مربوط به آن (سوم) می شویم. من کد بالا را اجرا و به شکل زیر تست کرده ام:

what is your age?0

You cannot enter zero

what is your age?0

You cannot enter zero

what is your age?as

You should enter a number

what is your age?25

Thank you

LOG

همانطور که می بینید بر اساس مقداری که وارد کرده ام، پیام متفاوتی را دریافت کرده ام تا زمانی که یک عدد غیر از صفر (۲۵) را وارد کرده ام و پیام thank you و log چاپ شده اند. آخرین بخش این دستور  finally است که پس از تمام قسمت try و except اجرا می شود:

while True:

    try:

        age = int(input('what is your age?'))

        10/age

    except ValueError:

        print('You should enter a number')

    except ZeroDivisionError:

        print('You cannot enter zero')

    else:

        print("Thank you")

        break

    finally:

        print("ALL DONE")




print("LOG")

به نظر شما با اجرای کد بالا چه اتفاقی می افتد؟

what is your age?25

Thank you

ALL DONE

LOG

همانطور که مشاهده می کنید ALL DONE (بلوک finally) بدون توجه به نتیجه اجرا خواهد شد، یعنی چه خطایی در کار باشد و چه خطایی در کار نباشد، قسمت finally باز هم اجرا خواهد شد.

در صورتی که بخواهید خودتان خطایی را پرتاب کرده و از روند اجرای کد جلوگیری کنید از دستوری مثل raise ValueError استفاده می کنیم:

while True:

    try:

        age = int(input('what is your age?'))

        10/age

        raise ValueError("THE VALUE IS WRONG")

    except ZeroDivisionError:

        print('You cannot enter zero')

    else:

        print("Thank you")

        break

    finally:

        print("ALL DONE")




print("LOG")

من در اینجا raise ValueError را اجرا کرده ام که یعنی یک exception از نوع ValueError را پرتاب کرده ایم. همچنین قسمت except ValueError را حذف کرده ام تا مدیریت خطایی برای این نوع خطا نباشد. با انجام این کار به خطا برخورد کرده و اجرای کدها متوقف می شود:

what is your age?25

ALL DONE

Traceback (most recent call last):

  File "/home/amir/Desktop/Roxo Academy/Python/test.py", line 5, in <module>

    raise ValueError("THE VALUE IS WRONG")

ValueError: THE VALUE IS WRONG

دریافت پیام خطا

فرض کنید بخواهیم تابعی بنویسیم که دو عدد را با هم جمع می کند:

def add_numbers(num1, num2):

    return num1 + num2







print(add_numbers('2', '4'))

با اجرای این کد عدد ۲۴ را دریافت می کنیم. چرا؟ به دلیل اینکه پارامتر های پاس داده شده از نوع رشته هستند و نه از نوع عدد بنابراین مفسر پایتون تصور می کند که شما قصد چسباندن رشته ها به یکدیگر را دارید. از طرفی می دانیم که اگر یکی از پارامتر ها رشته و دیگری عدد باشد این عملیات جمع انجام نخواهد شد. در چنین حالتی بهترین کار پیاده سازی try ... except درون خود تابع است:

def add_numbers(num1, num2):

    try:

        return num1 + num2

    except TypeError as err:

        print(err)







print(add_numbers('2', 4))

احتمالا شما هم متوجه شده اید که من دستور as را اضافه کرده ام. با اضافه کردن as err به مفسر پایتون گفته ایم که متن خطا را در قالب متغیری به نام err به ما ارسال کند (شما می توانید به جای err هر نام دیگری را انتخاب کنید). من err را چاپ کرده ام و در عین حال هنگام فراخوانی add_numbers یک آرگومان را به صورت رشته و دیگری را به صورت عدد پاس داده ام. با ارجاع کد بالا نتیجه زیر را می گیریم:

can only concatenate str (not "int") to str

None

جمله اول همان متن درون err بوده و به ما می گوید که توانایی چسباندن str (رشته) به int (عدد) را نداریم. None نیز برای این چاپ شده است که ما یک دستور print را در کدهایمان داشته ایم اما هیچ مقداری توسط تابع برگردانده نمی شود تا چاپ شود بنابراین None چاپ شده است.

در ضمن در نظر داشته باشید که می توانید دسته های خطا را با یکدیگر گروه بندی کنید. مثال:

def add_numbers(num1, num2):

    try:

        return num1 + num2

    except (TypeError, ZeroDivisionError) as err:

        print(err)

در این صورت اگر خطای ما از نوع typeError یا ZeroDivisionError باشد وارد این بلوک می شویم.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری پایتون حرفه‌ای توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.