مدیریت خطا (Exceptions)
کد بدون مدیریت خطا، کد ناتمام است. در این فصل یاد میگیریم چگونه با try/except خطاها را مدیریت کنیم و برنامههای پایدار بسازیم.
کد بدون مدیریت خطا، کد ناتمام است. در این فصل یاد میگیریم چگونه با try/except خطاها را مدیریت کنیم و برنامههای پایدار بسازیم.
چرا مدیریت خطا؟
# بدون مدیریت خطا
age = int(input("سن: ")) # اگر کاربر عدد ندهد؟ کرش!
result = 10 / int(input("مخرج: ")) # اگر صفر باشد؟ کرش!
# با مدیریت خطا
try:
age = int(input("سن: "))
print(f"سن شما {age} است")
except ValueError:
print("لطفاً یک عدد وارد کنید!")
ساختار try/except
try:
# کدی که ممکن است خطا بدهد
num = int(input("یک عدد: "))
result = 100 / num
print(f"نتیجه: {result}")
except ValueError:
print("ورودی عدد نیست!")
except ZeroDivisionError:
print("نمیتوان بر صفر تقسیم کرد!")
except Exception as e:
# هر خطای دیگر
print(f"خطای ناشناخته: {e}")
خطاهای رایج
| خطا | زمان رخ دادن |
|---|---|
ValueError |
تبدیل نوع نامعتبر: int("abc") |
TypeError |
عملیات روی نوع اشتباه: "a" + 5 |
NameError |
متغیر تعریف نشده |
IndexError |
اندیس خارج محدوده لیست |
KeyError |
کلید موجود نیست در dict |
AttributeError |
متد یا فیلد ناموجود |
FileNotFoundError |
فایل پیدا نشد |
PermissionError |
دسترسی نداریم |
ZeroDivisionError |
تقسیم بر صفر |
ImportError |
import نشد |
KeyboardInterrupt |
کاربر Ctrl+C زد |
RuntimeError |
خطای زمان اجرا |
تست خطاها
# ValueError
int("abc") # ValueError
# TypeError
"hello" + 5 # TypeError
len(42) # TypeError
# IndexError
nums = [1, 2, 3]
nums[10] # IndexError
# KeyError
d = {"name": "ali"}
d["age"] # KeyError
# FileNotFoundError
open("not_exist.txt") # FileNotFoundError
# AttributeError
"hello".reverse() # AttributeError (str.reverse وجود ندارد)
چندین except
try:
file = open("data.json")
data = json.load(file)
age = data["age"]
result = 100 / age
except FileNotFoundError:
print("فایل پیدا نشد")
except json.JSONDecodeError:
print("فایل JSON خراب است")
except KeyError:
print("کلید 'age' وجود ندارد")
except ZeroDivisionError:
print("سن نمیتواند صفر باشد")
except Exception as e:
# همه خطاهای دیگر
print(f"خطای غیرمنتظره: {type(e).__name__}: {e}")
چند خطا در یک except
try:
value = int(input("عدد: "))
result = 10 / value
except (ValueError, ZeroDivisionError) as e:
print(f"ورودی نامعتبر: {e}")
else و finally
try:
file = open("data.txt", "r", encoding="utf-8")
data = file.read()
except FileNotFoundError:
print("فایل پیدا نشد")
else:
# فقط اگر هیچ خطایی نبود اجرا میشود
print(f"محتوا: {data}")
print("خواندن موفق")
finally:
# همیشه اجرا میشود (با خطا یا بدون خطا)
print("اتمام عملیات")
try:
file.close()
except:
pass
الگوی صحیح
try:
file = open("data.txt", "r")
except FileNotFoundError:
print("ساخت فایل جدید...")
file = open("data.txt", "w")
else:
print("فایل پیدا شد")
finally:
file.close()
# روش بهتر: with statement!
try:
with open("data.txt", "r") as file:
data = file.read()
except FileNotFoundError:
print("فایل نبود")
پرتاب خطا با raise
def divide(a, b):
if b == 0:
raise ValueError("مخرج نمیتواند صفر باشد")
return a / b
try:
result = divide(10, 0)
except ValueError as e:
print(f"خطا: {e}")
# پرتاب مجدد
def process(data):
try:
# ...
result = risky_operation(data)
except Exception as e:
print(f"لاگ: {e}")
raise # پرتاب مجدد همان خطا
# تبدیل خطا
def load_config():
try:
with open("config.json") as f:
return json.load(f)
except FileNotFoundError as e:
raise RuntimeError("کانفیگ تنظیم نشده") from e
خطاهای سفارشی
class InsufficientFundsError(Exception):
"""خطای موجودی ناکافی"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(
f"موجودی ({balance}) برای برداشت {amount} کافی نیست"
)
class InvalidAgeError(ValueError):
"""خطای سن نامعتبر"""
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
def register(name, age):
if not (0 < age < 150):
raise InvalidAgeError(f"سن {age} نامعتبر است")
return {"name": name, "age": age}
# استفاده
try:
new_balance = withdraw(1000, 5000)
except InsufficientFundsError as e:
print(f"خطا: {e}")
print(f"موجودی: {e.balance}, درخواستی: {e.amount}")
الگوهای متداول
اعتبارسنجی ورودی با حلقه
while True:
try:
age = int(input("سن (1-120): "))
if 0 < age < 120:
break
print("سن باید بین ۱ تا ۱۲۰ باشد")
except ValueError:
print("لطفاً یک عدد وارد کنید")
print(f"سن شما: {age}")
EAFP – روش پایتونیک
EAFP = Easier to Ask for Forgiveness than Permission
data = {"name": "علی"}
# روش LBYL (Look Before You Leap) - غیرپایتونیک
if "age" in data:
print(data["age"])
else:
print("سن نداریم")
# روش EAFP (پایتونیک)
try:
print(data["age"])
except KeyError:
print("سن نداریم")
# یا سادهتر با get
print(data.get("age", "سن نداریم"))
Resource Cleanup
import sqlite3
# روش غلط
conn = sqlite3.connect("db.sqlite")
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
results = cursor.fetchall()
conn.close() # اگر بالا خطا بدهد، این اجرا نمیشود!
# روش درست
try:
conn = sqlite3.connect("db.sqlite")
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
results = cursor.fetchall()
finally:
conn.close() # حتماً اجرا میشود
# روش پایتونیک
with sqlite3.connect("db.sqlite") as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
results = cursor.fetchall()
# اتصال خودکار بسته میشود
بهترین شیوهها
✅ کارهای درست:
- خطاهای خاص را بگیرید (نه فقط
except) - پیام خطای دقیق و مفید بدهید
- خطاها را لاگ کنید
- از
finallyیاwithبرای cleanup استفاده کنید - خطاهای سفارشی را از
Exceptionارث ببرید
❌ کارهای اشتباه:
# بد: همه چیز را قورت میدهد
try:
risky()
except:
pass # ← هرگز این کار را نکنید!
# بد: گرفتن Exception عام بدون لاگ
try:
risky()
except Exception:
return None # ← دیباگ غیرممکن میشود
# خوب: حداقل لاگ کنید
import logging
try:
risky()
except Exception as e:
logging.exception("خطا در risky()")
raise
مثال واقعی: سیستم بانکی
class BankError(Exception):
"""کلاس پایه خطاهای بانکی"""
pass
class InsufficientFunds(BankError):
pass
class AccountNotFound(BankError):
pass
class InvalidAmount(BankError):
pass
class Bank:
def __init__(self):
self.accounts = {}
def create(self, account_id, balance=0):
if account_id in self.accounts:
raise BankError(f"حساب {account_id} وجود دارد")
if balance < 0:
raise InvalidAmount("موجودی منفی")
self.accounts[account_id] = balance
def withdraw(self, account_id, amount):
if account_id not in self.accounts:
raise AccountNotFound(f"حساب {account_id} نیست")
if amount <= 0:
raise InvalidAmount("مبلغ باید مثبت باشد")
if amount > self.accounts[account_id]:
raise InsufficientFunds(
f"موجودی: {self.accounts[account_id]}, درخواست: {amount}"
)
self.accounts[account_id] -= amount
return self.accounts[account_id]
bank = Bank()
bank.create("A001", 10000)
try:
new_balance = bank.withdraw("A001", 15000)
except InsufficientFunds as e:
print(f"موجودی ناکافی: {e}")
except AccountNotFound as e:
print(f"حساب نامعتبر: {e}")
except BankError as e:
print(f"خطای بانکی: {e}")
تمرین فصل
🏋️ تمرین ۱: برنامهای بنویسید که از کاربر دو عدد بگیرد و تقسیم آنها را با مدیریت کامل خطا (ورودی غیرعدد، تقسیم بر صفر) چاپ کند.
🏋️ تمرین ۲: تابعی بنویسید که فایل JSON را بخواند و در صورت بروز هر خطا (FileNotFoundError، JSONDecodeError)، پیام مناسب چاپ کند.
🏋️ تمرین ۳: یک خطای سفارشی به نام
InvalidEmailError بسازید و در تابعی برای اعتبارسنجی ایمیل استفاده کنید.
🏋️ تمرین ۴: ماشینحسابی بنویسید که تا زمانی که کاربر نخواهد، در حلقه میماند و در صورت بروز خطا، بهجای کرش، پیام نشان داده و دوباره میپرسد.
جمعبندی
- ساختار
try/exceptبرای مدیریت خطا - خطاهای رایج:
ValueError،TypeError،KeyError،FileNotFoundErrorو… - چندین
exceptبرای انواع خطا else(موفقیت) وfinally(همیشه)- پرتاب خطا با
raiseو زنجیره باraise from - ساخت خطاهای سفارشی
- الگوی EAFP در پایتون