مدیریت خطا در پایتون با try، except، else و finally

🗓 آخرین بهروزرسانی:
مدیریت خطا در پایتون کمک میکند برنامه بهجای توقف ناگهانی، وضعیتهای نامعتبر و خطاهای قابل پیشبینی را کنترل کند و پیام مناسبی به کاربر نمایش دهد. با ساختارهای try، except، else و finally میتوان بخشهای حساس برنامه را ایمنتر، خواناتر و قابل اعتمادتر طراحی کرد.
این مطلب، درس دهم از دوره رایگان پایتون سطح متوسط است. در درس قبلی با List Comprehension و ساخت و فیلتر لیستها آشنا شدیم. اکنون یاد میگیریم چگونه خطاهای زمان اجرا را تشخیص دهیم، فقط خطاهای مورد انتظار را مدیریت کنیم و در صورت نیاز خودمان خطا ایجاد کنیم.
خطا در پایتون چیست؟
هنگامی که پایتون نتواند یک دستور را بهدرستی اجرا کند، یک Exception یا استثنا ایجاد میشود. اگر این استثنا مدیریت نشود، اجرای برنامه متوقف و Traceback نمایش داده میشود.
نمونه تقسیم بر صفر:
number = 10 / 0این کد خطای زیر را ایجاد میکند:
ZeroDivisionError: division by zeroنمونه تبدیل رشته نامعتبر به عدد:
age = int("بیست")ValueError: invalid literal for int()تفاوت Syntax Error و Exception
خطای نحوی یا SyntaxError زمانی رخ میدهد که ساختار کد با قواعد زبان پایتون هماهنگ نیست:
if age >= 18
print("مجاز")در این مثال دونقطه فراموش شده است. برنامه پیش از شروع اجرای عادی متوقف میشود.
اما Exception معمولاً هنگام اجرای کدی رخ میدهد که از نظر نحوی صحیح است:
number = int(input("یک عدد وارد کنید: "))اگر کاربر متن نامعتبر وارد کند، کد از نظر نحوی درست است اما در زمان اجرا ValueError رخ میدهد.
ساختار پایه try و except
کدی که احتمال ایجاد خطا دارد داخل بلوک try قرار میگیرد. اگر خطای مشخصشده رخ دهد، بلوک except اجرا میشود:
try:
age = int(input("سن خود را وارد کنید: "))
print("سن ثبت شد:", age)
except ValueError:
print("سن باید بهصورت عدد صحیح وارد شود.")اگر ورودی معتبر باشد، کد داخل try اجرا میشود. اگر تبدیل با int() ناموفق باشد، برنامه متوقف نمیشود و پیام داخل except ValueError نمایش داده میشود.
جریان اجرای try و except
- پایتون اجرای دستورات داخل
tryرا آغاز میکند. - اگر هیچ خطایی رخ ندهد، بلوک
exceptنادیده گرفته میشود. - اگر خطای هماهنگ با
exceptرخ دهد، اجرای باقی دستوراتtryمتوقف میشود. - بلوک
exceptاجرا میشود. - پس از پایان ساختار، برنامه از دستور بعدی ادامه پیدا میکند.
try:
number = int("abc")
print("این خط اجرا نمیشود")
except ValueError:
print("تبدیل مقدار ممکن نبود")
print("برنامه ادامه دارد")مدیریت خطای مشخص
بهترین روش این است که فقط خطاهایی را مدیریت کنیم که انتظار وقوع آنها را داریم:
try:
number = int(input("عدد وارد کنید: "))
except ValueError:
print("ورودی باید عدد صحیح باشد.")نوشتن نوع خطا باعث میشود اشتباههای برنامهنویسی بهصورت ناخواسته پنهان نشوند.
چرا except خالی مناسب نیست؟
کد زیر تقریباً هر خطایی را میگیرد:
try:
result = calculate_result()
except:
print("خطایی رخ داد")این روش ممکن است خطاهای مهم، توقف کاربر با صفحهکلید یا مشکلاتی را که باید بررسی شوند پنهان کند. بهتر است نوع خطا مشخص باشد:
except ValueError:
print("مقدار نامعتبر است")در مواردی که واقعاً باید خطاهای معمول برنامه را بهصورت عمومی مدیریت کنیم، از Exception استفاده میکنیم:
try:
run_operation()
except Exception as error:
print("خطای غیرمنتظره:", error)با این حال، گرفتن عمومی Exception باید در سطح مناسب برنامه و همراه با ثبت جزئیات انجام شود، نه برای پنهانکردن همه خطاها.
گرفتن چند نوع خطا با چند except
یک بخش کد ممکن است چند خطای متفاوت ایجاد کند:
try:
number1 = int(input("عدد اول: "))
number2 = int(input("عدد دوم: "))
result = number1 / number2
print(result)
except ValueError:
print("هر دو ورودی باید عدد صحیح باشند.")
except ZeroDivisionError:
print("تقسیم بر صفر امکانپذیر نیست.")هر except مسئول یک نوع خطاست و میتواند پیام یا رفتار اختصاصی داشته باشد.
گرفتن چند خطا در یک except
اگر چند خطا رفتار یکسانی دارند، میتوان آنها را داخل یک تاپل قرار داد:
try:
value = data[index]
except (IndexError, TypeError):
print("دسترسی به داده ممکن نیست.")این روش زمانی مناسب است که برای همه خطاهای مشخصشده یک پاسخ یکسان داریم.
دسترسی به پیام خطا با as
با استفاده از as میتوان شیء خطا را داخل یک متغیر دریافت کرد:
try:
number = int("Python")
except ValueError as error:
print("نوع خطا:", type(error).__name__)
print("پیام خطا:", error)پیام اصلی خطا برای ثبت گزارش و عیبیابی مفید است. در برنامههای واقعی بهتر است پیام فنی کامل فقط در گزارش داخلی ذخیره شود و پیام سادهتری به کاربر نمایش داده شود.
ترتیب exceptها مهم است
خطاهای اختصاصی باید پیش از خطاهای عمومی قرار گیرند:
try:
number = int(input("عدد: "))
except ValueError:
print("ورودی عددی نیست")
except Exception as error:
print("خطای دیگر:", error)اگر Exception ابتدا قرار گیرد، بسیاری از خطاهای اختصاصی را زودتر میگیرد و بلوکهای بعدی عملاً قابل دسترسی نخواهند بود.
بلوک else در مدیریت خطا
بلوک else فقط زمانی اجرا میشود که هیچ خطایی در try رخ نداده باشد:
try:
number = int(input("عدد وارد کنید: "))
except ValueError:
print("ورودی نامعتبر است")
else:
print("عدد معتبر:", number)مزیت else این است که کد موفقیت را از بخش خطاپذیر جدا میکند.
چرا همه کد را داخل try قرار ندهیم؟
بلوک try باید تا حد ممکن کوچک باشد. فقط دستورهایی را داخل آن قرار دهید که انتظار دارید خطای موردنظر را ایجاد کنند:
try:
price = float(input("قیمت: "))
except ValueError:
print("قیمت باید عدد باشد")
else:
final_price = price * 1.09
print("قیمت نهایی:", final_price)اگر محاسبات بعدی خطای دیگری داشته باشند، جدا از خطای تبدیل ورودی قابل تشخیص خواهند بود.
بلوک finally در پایتون
بلوک finally تقریباً در هر حالت اجرا میشود؛ چه خطا رخ دهد، چه رخ ندهد:
try:
number = int(input("عدد: "))
except ValueError:
print("ورودی نامعتبر")
finally:
print("پایان عملیات")finally برای آزادکردن منابع و انجام عملیات پاکسازی مناسب است؛ مانند بستن فایل، اتصال شبکه یا پایگاه داده.
ساختار کامل try، except، else و finally
try:
number1 = float(input("عدد اول: "))
number2 = float(input("عدد دوم: "))
result = number1 / number2
except ValueError:
print("ورودیها باید عدد باشند.")
except ZeroDivisionError:
print("عدد دوم نمیتواند صفر باشد.")
else:
print("نتیجه:", result)
finally:
print("عملیات محاسبه پایان یافت.")در این ساختار:
tryکد خطاپذیر را اجرا میکند.exceptخطاهای مشخص را مدیریت میکند.elseمسیر موفق را اجرا میکند.finallyعملیات نهایی و پاکسازی را انجام میدهد.
نمونههای رایج Exception در پایتون
| خطا | علت رایج |
|---|---|
ValueError | نوع کلی مقدار درست است اما محتوای آن قابل قبول نیست؛ مانند int("abc") |
TypeError | عملیات روی نوع داده نامناسب انجام شده است |
ZeroDivisionError | تقسیم یا باقیمانده بر صفر |
IndexError | اندیس خارج از محدوده لیست یا تاپل |
KeyError | کلید موردنظر در دیکشنری وجود ندارد |
NameError | نام متغیر یا تابع تعریف نشده است |
AttributeError | شیء ویژگی یا متد درخواستشده را ندارد |
FileNotFoundError | فایل در مسیر مشخصشده پیدا نشده است |
PermissionError | مجوز لازم برای دسترسی به فایل یا منبع وجود ندارد |
ImportError | واردکردن ماژول یا نام موردنظر ناموفق بوده است |
مثال ValueError
try:
quantity = int(input("تعداد محصول: "))
except ValueError:
print("تعداد باید عدد صحیح باشد.")مثال TypeError
try:
result = "10" + 5
except TypeError as error:
print("نوع دادهها با عملیات سازگار نیست:", error)مثال IndexError
skills = ["Python", "Git", "SQL"]
try:
print(skills[5])
except IndexError:
print("چنین اندیسی در لیست وجود ندارد.")مثال KeyError و روش جایگزین
user = {"name": "مهران"}
try:
print(user["city"])
except KeyError:
print("شهر کاربر ثبت نشده است.")برای کلیدهای اختیاری معمولاً استفاده از get() سادهتر است:
city = user.get("city", "ثبت نشده")اما اگر نبودن کلید یک وضعیت غیرعادی و مهم باشد، مدیریت KeyError میتواند مناسبتر باشد.
اعتبارسنجی ورودی با حلقه و try
یکی از کاربردهای مهم مدیریت خطا، درخواست دوباره ورودی تا زمان دریافت مقدار معتبر است:
while True:
try:
age = int(input("سن خود را وارد کنید: "))
except ValueError:
print("لطفاً فقط عدد صحیح وارد کنید.")
continue
if age < 0:
print("سن نمیتواند منفی باشد.")
continue
break
print("سن ثبتشده:", age)try خطای تبدیل نوع را مدیریت میکند و شرط جداگانه قوانین کسبوکار را بررسی میکند.
Exception جایگزین همه شرطها نیست
هر وضعیت نامعتبر الزاماً Exception نیست. برای قوانین عادی برنامه از شرط استفاده میکنیم:
if balance < amount:
print("موجودی کافی نیست")Exception بیشتر برای وضعیتهایی مناسب است که اجرای عادی عملیات را مختل میکنند یا تابع نمیتواند نتیجه معتبر تولید کند.
ایجاد خطا با raise
با کلمه کلیدی raise میتوانیم خودمان یک Exception ایجاد کنیم:
def calculate_discount(price, percent):
if price < 0:
raise ValueError("قیمت نمیتواند منفی باشد")
if not 0 <= percent <= 100:
raise ValueError("درصد تخفیف باید بین صفر تا صد باشد")
return price - (price * percent / 100)فراخوانی تابع:
try:
final_price = calculate_discount(500000, 120)
except ValueError as error:
print(error)raise قرارداد تابع را شفاف میکند: اگر ورودی نامعتبر باشد، تابع بهجای برگرداندن نتیجه اشتباه یک خطای مشخص ایجاد میکند.
ایجاد دوباره همان خطا
گاهی میخواهیم خطا را ثبت کنیم و سپس اجازه دهیم به سطح بالاتر منتقل شود:
def process_order():
try:
save_order()
except Exception as error:
print("ثبت داخلی خطا:", error)
raiseنوشتن raise بدون نام خطا داخل except همان Exception جاری را دوباره ایجاد میکند.
زنجیرهکردن خطاها با raise from
میتوان یک خطای سطح پایین را به خطایی معنادارتر تبدیل کرد و علت اصلی را حفظ کرد:
def parse_quantity(text):
try:
return int(text)
except ValueError as error:
raise ValueError("تعداد محصول معتبر نیست") from errorاین روش برای طراحی لایههای برنامه و حفظ اطلاعات عیبیابی مفید است.
ساخت Exception اختصاصی
در پروژههای بزرگ میتوان خطای اختصاصی ساخت:
class InsufficientBalanceError(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientBalanceError("موجودی حساب کافی نیست")
return balance - amountمدیریت خطای اختصاصی:
try:
new_balance = withdraw(500000, 750000)
except InsufficientBalanceError as error:
print(error)برای پروژههای کوچک معمولاً خطاهای داخلی مانند ValueError کافی هستند، اما خطاهای اختصاصی در سیستمهای بزرگ معنای دقیقتری ایجاد میکنند.
مدیریت خطا داخل تابع یا بیرون تابع؟
اصل کلی این است که خطا در سطحی مدیریت شود که تصمیم مناسبی درباره آن میگیرد.
تابع سطح پایین میتواند خطا ایجاد کند:
def divide(a, b):
if b == 0:
raise ZeroDivisionError("مخرج نمیتواند صفر باشد")
return a / bبخش رابط کاربر میتواند پیام نمایش دهد:
try:
result = divide(10, 0)
except ZeroDivisionError as error:
print("محاسبه انجام نشد:", error)این جداسازی باعث میشود تابع محاسباتی به نحوه نمایش پیام وابسته نباشد.
Try تودرتو
استفاده از ساختارهای تودرتو ممکن است، اما باید با احتیاط انجام شود:
try:
number = int(input("عدد: "))
try:
result = 100 / number
except ZeroDivisionError:
print("عدد نباید صفر باشد")
except ValueError:
print("ورودی باید عدد باشد")در بسیاری از موارد چند except در یک ساختار یا تقسیم کد به چند تابع خواناتر است.
return در کنار finally
finally حتی زمانی که تابع در حال بازگرداندن مقدار است اجرا میشود:
def example():
try:
return "نتیجه try"
finally:
print("finally اجرا شد")اما نوشتن return داخل finally معمولاً خطرناک است، زیرا میتواند مقدار برگشتی قبلی یا حتی Exception را پنهان کند:
def bad_example():
try:
return 10
finally:
return 20در این مثال مقدار نهایی ۲۰ است. بهتر است finally فقط برای پاکسازی استفاده شود.
استفاده از finally برای منابع
file = None
try:
file = open("data.txt", "r", encoding="utf-8")
content = file.read()
except FileNotFoundError:
print("فایل پیدا نشد")
finally:
if file is not None:
file.close()برای فایلها روش بهتر استفاده از with است که بستن فایل را بهصورت خودکار انجام میدهد؛ این موضوع در درس بعدی کامل بررسی میشود.
ثبت خطا بهجای print
در پروژههای واقعی بهتر است جزئیات فنی خطا ثبت شوند. ماژول logging برای این کار مناسب است:
import logging
logging.basicConfig(level=logging.ERROR)
try:
result = 10 / 0
except ZeroDivisionError:
logging.exception("خطا هنگام محاسبه")logging.exception() داخل بلوک except پیام و Traceback را ثبت میکند.
بهترین روشهای مدیریت خطا
- فقط خطاهای مورد انتظار را مدیریت کنید.
- بلوک
tryرا تا حد امکان کوچک نگه دارید. - بهجای
except:از خطای مشخص استفاده کنید. - برای مسیر موفق از
elseاستفاده کنید. finallyرا برای پاکسازی منابع نگه دارید.- پیام مناسب برای کاربر و جزئیات فنی برای گزارش داخلی را جدا کنید.
- خطا را بدون دلیل نادیده نگیرید.
- برای قوانین عادی برنامه از شرط و برای شکست عملیات از Exception استفاده کنید.
- در توابع عمومی، نوع خطا و شرایط ایجاد آن را مستند کنید.
- در پروژههای بزرگ از خطاهای اختصاصی معنادار استفاده کنید.
اشتباه رایج: استفاده از pass در except
try:
run_task()
except Exception:
passاین کد همه خطاهای معمول را بیصدا پنهان میکند. اگر نادیدهگرفتن یک خطا واقعاً بخشی از طراحی است، دلیل آن را مستند و دامنه خطا را بسیار محدود کنید:
try:
optional_value = settings["theme"]
except KeyError:
optional_value = "light"اشتباه رایج: پیام خطای گمراهکننده
try:
result = 10 / number
except Exception:
print("ورودی عدد نیست")ممکن است خطا اصلاً مربوط به نوع ورودی نباشد. نوع Exception باید با پیام هماهنگ باشد:
except ZeroDivisionError:
print("عدد نمیتواند صفر باشد")اشتباه رایج: try بسیار بزرگ
وقتی دهها دستور داخل یک try قرار میگیرند، مشخص نیست کدام دستور خطا ایجاد کرده است. کد را به مراحل کوچکتر و توابع مستقل تقسیم کنید.
پروژه اول: ماشینحساب مقاوم در برابر خطا
در این پروژه یک ماشینحساب میسازیم که ورودی نامعتبر، عملگر ناشناخته و تقسیم بر صفر را مدیریت میکند.
def calculate(number1, number2, operator):
if operator == "+":
return number1 + number2
if operator == "-":
return number1 - number2
if operator == "*":
return number1 * number2
if operator == "/":
if number2 == 0:
raise ZeroDivisionError("تقسیم بر صفر مجاز نیست")
return number1 / number2
raise ValueError("عملگر معتبر نیست")
while True:
try:
first = float(input("عدد اول: "))
operator = input("عملگر (+ - * /): ").strip()
second = float(input("عدد دوم: "))
result = calculate(first, second, operator)
except ValueError as error:
print("خطای ورودی:", error)
except ZeroDivisionError as error:
print("خطای محاسبه:", error)
else:
print("نتیجه:", result)
finally:
print("--- پایان این محاسبه ---")
again = input("محاسبه دیگری انجام شود؟ (y/n): ").strip().lower()
if again != "y":
breakنکات پروژه ماشینحساب
- تابع
calculateمسئول منطق محاسبه است. - ورودی و نمایش پیام در حلقه اصلی انجام میشود.
- عملگر نامعتبر با
ValueErrorگزارش میشود. - تقسیم بر صفر خطای اختصاصی خود را دارد.
- مسیر موفق در
elseقرار گرفته است. finallyپایان هر تلاش را نمایش میدهد.
پروژه دوم: ثبت سفارش با اعتبارسنجی و خطای اختصاصی
class OrderValidationError(Exception):
pass
def create_order(product_name, price, quantity, discount=0):
if not product_name.strip():
raise OrderValidationError("نام محصول خالی است")
if price <= 0:
raise OrderValidationError("قیمت باید بیشتر از صفر باشد")
if quantity <= 0:
raise OrderValidationError("تعداد باید بیشتر از صفر باشد")
if not 0 <= discount <= 100:
raise OrderValidationError("تخفیف باید بین صفر تا صد باشد")
subtotal = price * quantity
final_total = subtotal - (subtotal * discount / 100)
return {
"product": product_name,
"quantity": quantity,
"subtotal": subtotal,
"discount": discount,
"final_total": final_total
}
try:
name = input("نام محصول: ")
price = float(input("قیمت: "))
quantity = int(input("تعداد: "))
discount = float(input("درصد تخفیف: "))
order = create_order(name, price, quantity, discount)
except ValueError:
print("قیمت، تعداد و تخفیف باید عدد معتبر باشند.")
except OrderValidationError as error:
print("سفارش ثبت نشد:", error)
else:
print("سفارش با موفقیت ثبت شد")
print(order)
finally:
print("فرایند ثبت سفارش پایان یافت")مدیریت خطا در برنامههایی که به اینترنت وصل میشوند اهمیت بیشتری دارد. در پروژه برنامه هواشناسی با پایتون خطاهای timeout، قطع اتصال و شهر ناموجود را با یک Exception اختصاصی مدیریت میکنیم.
تمرینهای درس
- برنامهای بنویسید که دو عدد دریافت کند و خطاهای
ValueErrorوZeroDivisionErrorرا جداگانه مدیریت کند. - تابعی بسازید که یک اندیس و یک لیست دریافت کند و در صورت نامعتبر بودن اندیس پیام مناسبی برگرداند.
- برنامهای بنویسید که کلیدی را از دیکشنری دریافت کند و خطای
KeyErrorرا مدیریت کند. - با حلقه
whileآنقدر از کاربر عدد بخواهید تا مقدار معتبر وارد کند. - تابعی برای محاسبه سن بسازید که برای سال تولد آینده
ValueErrorایجاد کند. - برنامهای بنویسید که یک فایل را باز کند و خطاهای
FileNotFoundErrorوPermissionErrorرا جداگانه مدیریت کند. - یک Exception اختصاصی با نام
InvalidScoreErrorبسازید و برای نمره خارج از بازه صفر تا بیست استفاده کنید. - مثالی بنویسید که در آن
elseفقط پس از تبدیل موفق ورودی اجرا شود. - مثالی بسازید که
finallyدر هر دو حالت موفق و ناموفق اجرا شود. - پروژه ماشینحساب را توسعه دهید تا عملگرهای توان، باقیمانده و تقسیم صحیح را نیز پشتیبانی کند.
📌 همچنین بخوانید: بازار کار پایتون در ایران، دستورهای break، continue و pass در پایتون و ساخت ربات تلگرام با پایتون
پرسشهای متداول
آیا باید برای همه دستورات از try استفاده کنیم؟
خیر. فقط بخشهایی را داخل try قرار دهید که خطای قابل پیشبینی دارند و برنامه میتواند واکنش معناداری به آن نشان دهد.
تفاوت except خالی با except Exception چیست؟
except: دامنه بسیار گستردهای دارد و حتی برخی سیگنالهای مهم توقف را میگیرد. except Exception بیشتر خطاهای عادی برنامه را میگیرد، اما باز هم باید فقط در جای مناسب استفاده شود.
چه زمانی از else استفاده کنیم؟
وقتی میخواهیم کدی فقط در صورت موفقبودن کامل بلوک try اجرا شود. این کار اندازه try را کوچک و مسیر موفق را واضح میکند.
finally چه زمانی اجرا نمیشود؟
در اجرای عادی تقریباً همیشه اجرا میشود. توقف ناگهانی فرایند، خاموششدن سیستم یا پایان اجباری مفسر میتواند مانع اجرای آن شود؛ بنابراین برای عملیات حیاتی فقط به finally وابسته نباشید.
آیا raise برنامه را متوقف میکند؟
raise یک Exception ایجاد میکند. اگر در سطح بالاتر مدیریت نشود، اجرای برنامه متوقف میشود؛ اگر مدیریت شود، برنامه میتواند از مسیر تعریفشده ادامه پیدا کند.
برای ورودی نامعتبر شرط بهتر است یا Exception؟
برای تبدیل نوع و شکست عملیات از Exception استفاده کنید. برای قوانین عادی مانند بازه سن، موجودی کافی یا فعالبودن حساب، شرطها معمولاً واضحتر هستند؛ هرچند یک تابع میتواند پس از تشخیص وضعیت نامعتبر با شرط، Exception معناداری ایجاد کند.
جمعبندی
در این درس یاد گرفتیم که try کد خطاپذیر را اجرا میکند، except خطاهای مشخص را مدیریت میکند، else مسیر موفق و finally عملیات نهایی را انجام میدهد. همچنین با خطاهای رایج، raise، خطاهای اختصاصی، اعتبارسنجی ورودی، ثبت خطا و اصول طراحی کد مقاوم آشنا شدیم.
درس بعدی: خواندن و نوشتن فایلهای متنی در پایتون — در حال آمادهسازی
بازگشت به فهرست دوره پایتون سطح متوسط
ادامه مسیر یادگیری پایتون
برای یادگیری منظم، آموزشهای مرتبط زیر را بهترتیب دنبال کنید:
برای ذخیره پیشرفت در همه دستگاهها و دریافت گواهی پایان مسیر، رایگان ثبتنام کن یا وارد شو.