خطاها و استثناها
برنامه خوب آن نیست که خطا ندارد، آن است که وقتی خطا رخ میدهد نمیترکد. این فصل درباره این است که چطور به جای خراب شدن، واکنش نشان دهیم.
۱. سه نوع مشکل
| نوع | کی معلوم میشود | مثال |
|---|---|---|
| خطای نگارشی | پیش از اجرا | فراموش کردن دونقطه یا گیومه |
| استثنا | هنگام اجرا | تقسیم بر صفر، فایل ناموجود |
| خطای منطقی | هرگز خودکار معلوم نمیشود | فرمول غلط، پرانتز جاافتاده |
این فصل درباره نوع دوم است: استثنا. کدی که از نظر نگارشی درست است ولی هنگام اجرا به مشکل میخورد. خطای منطقی هیچ ابزاری ندارد جز فکر کردن و آزمایش کردن.
وقتی استثنایی رخ دهد و کسی آن را نگیرد، پایتون برنامه را متوقف میکند و یک ردیابی چاپ میکند. آن را از آخر بخوانید: خط آخر میگوید چه شد، خطهای بالاتر میگویند از کجا آمده است.
۲. اولین try و except
# Without a try, this line stops the whole program:
# x = 10 / 0
# ZeroDivisionError: division by zero
# 'try' contains the risky code, 'except' says what to do when it fails.
try:
x = 10 / 0
print(x)
except ZeroDivisionError:
print('Division by zero error')
print('The program continues normally.')
اولین try: نمونه 01_zero_division.py
Division by zero error The program continues normally.
شرح مثال
- کد پرخطر داخل
tryمیرود و واکنش داخلexcept. - به محض رخ دادن خطا، بقیه بلوک
tryرها میشود و اجرا بهexceptمیپرد. به همین دلیل آنprint(x)هرگز اجرا نشد. - بعد از بلوک، برنامه عادی ادامه پیدا میکند. این تفاوت «مدیریتشده» و «خراب شدن» است.
- داخل
tryفقط کدی بگذارید که واقعاً ممکن است خطا بدهد. بلوکtryبزرگ، خطاهای غیرمنتظره را هم میبلعد.
۳. گرفتن پیام خطا
# int() raises ValueError when the text is not a number.
text = 'Kadoos'
try:
age = int(text)
print(age)
except ValueError as e:
# 'as e' gives us the exception object, which contains the message.
print("Can't convert this string to an integer!")
print('The message of Python was:', e)
گرفتن پیام: نمونه 02_value_error.py
Can't convert this string to an integer! The message of Python was: invalid literal for int() with base 10: 'Kadoos'
با as e خود شیء استثنا را میگیرید. چاپ کردنش پیام اصلی
پایتون را میدهد و type(e).__name__ نام نوع خطا را. برای
گزارشگیری بسیار مفید است.
۴. استثناهای پرتکرار
| استثنا | کی رخ میدهد |
|---|---|
ValueError | نوع درست است ولی مقدار بیمعناست، مثل int('abc') |
TypeError | نوع غلط است، مثل 'a' + 1 |
IndexError | اندیس بیرون از لیست |
KeyError | کلید ناموجود در دیکشنری |
FileNotFoundError | فایل وجود ندارد |
PermissionError | اجازه دسترسی به فایل نیست |
ZeroDivisionError | تقسیم بر صفر |
AttributeError | شیء چنین صفت یا متدی ندارد |
ImportError | ماژول پیدا نشد |
items = [10, 20, 30]
try:
print(items[5])
except IndexError:
print('This index does not exist!')
# The same protection without an exception:
index = 5
if 0 <= index < len(items):
print(items[index])
else:
print('index out of range (checked with an if)')
خطای اندیس: نمونه 03_index_error.py
# Opening a file that does not exist raises FileNotFoundError.
try:
with open('myfile.txt', encoding='utf-8') as f:
print(f.read())
except FileNotFoundError:
print('File not found.')
except PermissionError:
print('You are not allowed to read this file.')
فایل ناموجود: نمونه 05_file_not_found.py
هر دو درستاند. وقتی میشود راحت جلوگیری کرد، مثل بررسی اندیس یا
dict.get، همان بهتر است. ولی برای فایل، بررسی کردن
کافی نیست: ممکن است بین لحظه بررسی و لحظه باز کردن، فایل پاک شود.
قاعده عملی: برای منطق برنامه if، برای چیزهایی که
بیرون از کنترل شما هستند مثل فایل و شبکه، try.
۵. چند except برای یک try
# One 'try' can have several 'except' blocks: Python runs the first one
# that matches the exception that happened.
text = 'AA' # try also with '10' and with '1'
numbers = [1, 2, 3]
try:
index = int(text) # may raise ValueError
print(numbers[index]) # may raise IndexError
except ValueError:
print('Conversion error!')
except IndexError:
print('Index error!')
# Several exceptions can share the same block.
try:
index = int(text)
print(numbers[index])
except (ValueError, IndexError) as e:
print('Something went wrong:', type(e).__name__, '-', e)
چند except: نمونه 06_several_excepts.py
پایتون از بالا به پایین اولین except مناسب را اجرا میکند،
درست مثل elif. اگر چند خطا واکنش یکسانی دارند، میتوانید
همهشان را در یک تاپل بگذارید.
۶. else و finally
# The complete shape:
# try -> the risky code
# except -> what to do when it fails
# else -> runs only when NOTHING failed
# finally -> runs ALWAYS (success or failure), used to clean up
user_input = input('Enter a number: ')
try:
number = int(user_input)
x = 10 / number
print(x)
except ZeroDivisionError:
print('Division by zero error')
except ValueError:
print('That was not a number')
else:
print('The division is done!')
finally:
print('Error or no error, this always runs.')
ساختار کامل: نمونه 07_else_finally.py
| بخش | کی اجرا میشود |
|---|---|
try | همیشه، تا وقتی خطا رخ دهد |
except | فقط وقتی آن نوع خطا رخ داده باشد |
else | فقط وقتی هیچ خطایی رخ نداده باشد |
finally | همیشه، چه خطا باشد چه نباشد |
finally برای پاکسازی است: بستن فایل، قطع اتصال پایگاه داده، آزاد کردن منابع. حتی اگر داخل try دستور return باشد، باز هم finally اجرا میشود. البته با وجود with، بیشتر کاربردهای قدیمیاش دیگر ضرورتی ندارد.
۷. چرا except خالی بد است
# A bare 'except:' catches EVERYTHING, even the mistakes in your own code
# and Ctrl-C. The bug then becomes invisible - never do this.
try:
x = 10 / 0
except:
print('Something is wrong!') # but WHAT exactly? we have no idea
# Catch the exception you expect ...
try:
x = 10 / 0
except ZeroDivisionError as e:
print('Clear message:', e)
# ... or, if you really must catch everything, at least keep the object.
try:
x = 10 / 0
except Exception as e:
print(f'{type(e).__name__}: {e}')
except خالی: نمونه 08_bare_except.py
except: خالی همه چیز را میگیرد، از جمله غلط تایپی
خودتان و حتی Ctrl + C کاربر. نتیجه این است که باگ
نامرئی میشود: برنامه پیام مبهمی میدهد و شما
هفتهها نمیفهمید مشکل کجاست. همیشه نوع خطا را بنویسید. اگر واقعاً
مجبورید همه چیز را بگیرید، دستکم except Exception as e
بنویسید و پیام را چاپ کنید.
۸. دستور raise
# 'raise' creates an exception ourselves. It is how a function says
# "the value you gave me makes no sense".
def set_age(age: int) -> int:
if age < 0:
raise ValueError("Age can't be negative!")
if age > 130:
raise ValueError('Age is unrealistic!')
return age
try:
set_age(-5)
except ValueError as e:
print('Refused:', e)
# Re-raising after logging is a common pattern: we note the problem,
# but we let the caller decide what to do.
def read_config(path: str) -> str:
try:
with open(path, encoding='utf-8') as f:
return f.read()
except FileNotFoundError:
print(f'[warning] {path} is missing')
raise # send the same exception further up
try:
read_config('nothing.cfg')
except FileNotFoundError:
print('The caller handled it.')
دستور raise: نمونه 09_raise.py
شرح مثال
raiseیعنی «من نمیتوانم این کار را انجام دهم». تابع به جای برگرداندن مقدار بیمعنی، خطا میاندازد.- چرا تابع خودش پیام چاپ نمیکند؟ چون تابع نمیداند برنامه از کجا صدا زده شده است. شاید برنامه گرافیکی باشد و پیام باید در پنجره نشان داده شود. تصمیمگیری وظیفه صداکننده است.
raiseخالی داخل یکexcept، همان خطا را دوباره بالا میفرستد. الگوی «ثبت کن و پاس بده» است.- نوع خطا را مناسب انتخاب کنید:
ValueErrorبرای مقدار نامعتبر،TypeErrorبرای نوع نادرست. در فصل بیست و چهارم میبینید که میشود کلاس خطای اختصاصی هم ساخت.
۹. تابع خواندن امن عدد
مفیدترین چیز این فصل. این دو تابع را در هر برنامهای که ورودی میگیرد میتوانید کپی کنید.
'''The most useful pattern of this session: reading a valid number.'''
def read_number(message: str) -> float:
'Ask until the user really types a number.'
while True:
user_input = input(message)
try:
return float(user_input)
except ValueError:
print('That is not a number, try again.')
def read_int(message: str, minimum: int, maximum: int) -> int:
'Ask until the user types a whole number inside the range.'
while True:
try:
value = int(input(message))
except ValueError:
print('Please type digits only.')
continue
if value < minimum or value > maximum:
print(f'The value must be between {minimum} and {maximum}.')
continue
return value
def main() -> None:
a = read_number('Enter a number: ')
b = read_number('Enter another number: ')
print('Sum:', a + b)
age = read_int('Enter your age (1-120): ', 1, 120)
print('Your age is', age)
if __name__ == '__main__':
main()
خواندن امن: نمونه 10_safe_input.py
ترکیب سه چیز است: حلقه بیپایان فصل هشتم، try این فصل، و
return که هم مقدار را میدهد و هم حلقه را تمام میکند.
مقایسه کنید با نسخه فصل هشتم که با isdigit() کار میکرد:
این نسخه اعشاری و منفی را هم درست مدیریت میکند.
۱۰. دستور assert
# 'assert condition, message' stops the program with an AssertionError when the
# condition is False. It is a tool for the DEVELOPER, not for the user:
# it documents an assumption and catches a bug early.
def average(numbers: list[float]) -> float:
assert len(numbers) > 0, 'average() needs at least one number'
return sum(numbers) / len(numbers)
print(average([10, 20, 30]))
try:
average([])
except AssertionError as e:
print('AssertionError:', e)
# Careful: assertions disappear when Python is run with the -O option,
# so never use assert to validate what a USER typed - use if/raise for that.
دستور assert: نمونه 11_assert.py
assert | raise | |
|---|---|---|
| برای چه کسی | برنامهنویس | کاربر و صداکننده تابع |
| چه میگوید | «این نباید هرگز رخ دهد» | «ورودی نامعتبر است» |
| در حالت بهینه | حذف میشود | باقی میماند |
| اعتبارسنجی ورودی کاربر | هرگز | بله |
نکته حیاتی: اگر پایتون با گزینه -O اجرا شود، همه
assert ها حذف میشوند. پس هرگز برای
بررسی ورودی کاربر یا برای امنیت به آن تکیه نکنید.
۱۱. ماژول logging
# print() is fine while learning, but a real program uses 'logging':
# every message has a level, a time stamp, and can be sent to a file.
import logging
logging.basicConfig(
level=logging.DEBUG, # show everything from DEBUG upwards
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%H:%M:%S',
# filename='app.log', # uncomment to write into a file
)
logger = logging.getLogger(__name__)
def divide(a: float, b: float) -> float | None:
logger.debug('divide(%s, %s) was called', a, b)
try:
result = a / b
except ZeroDivisionError:
# exception() also prints the traceback - perfect inside an except block
logger.exception('division by zero')
return None
logger.info('the result is %s', result)
return result
def main() -> None:
logger.info('program started')
divide(10, 2)
divide(10, 0)
logger.warning('this is a warning')
logger.error('this is an error')
logger.info('program finished')
if __name__ == '__main__':
main()
ماژول logging: نمونه 12_logging.py
14:32:10 [INFO] program started 14:32:10 [DEBUG] divide(10, 2) was called 14:32:10 [INFO] the result is 5.0 14:32:10 [DEBUG] divide(10, 0) was called 14:32:10 [ERROR] division by zero Traceback (most recent call last): ... ZeroDivisionError: division by zero 14:32:10 [WARNING] this is a warning
| سطح | کی به کار میرود |
|---|---|
DEBUG | جزئیات فنی، فقط هنگام توسعه |
INFO | گزارش کارهای عادی |
WARNING | چیزی عجیب است ولی برنامه کار میکند |
ERROR | کاری انجام نشد |
CRITICAL | برنامه نمیتواند ادامه دهد |
چرا logging بهتر از print است
- هر پیام سطح دارد، پس میتوانید با یک تنظیم، پیامهای جزئی را خاموش کنید بدون اینکه کد را عوض کنید.
- زمان و نام ماژول خودکار اضافه میشود.
- با یک پارامتر میشود همه چیز را به جای صفحه، در فایل نوشت.
logger.exceptionداخلexceptردیابی کامل را هم ثبت میکند که برای عیبیابی طلاست.- دقت کنید که پیام با
%sو آرگومان جدا نوشته شده، نه با f-string. اینطور اگر آن سطح خاموش باشد، رشته اصلاً ساخته نمیشود.
پوشه S19-errors-and-exceptions شامل نمونههای
01_zero_division.py تا 12_logging.py.
خلاصه فصل
| ابزار | وظیفه آن |
|---|---|
try / except | گرفتن خطا به جای خراب شدن برنامه |
except X as e | دسترسی به پیام و نوع خطا |
except (A, B) | یک واکنش برای چند خطا |
else | وقتی هیچ خطایی نبود |
finally | همیشه. برای پاکسازی |
except: خالی | هرگز. باگ را نامرئی میکند |
raise ValueError(msg) | اعلام خطا از داخل تابع |
raise خالی | فرستادن همان خطا به لایه بالاتر |
assert | بررسی فرض برنامهنویس. برای ورودی کاربر نه |
logging | جایگزین حرفهای print، با سطح و زمان و فایل |
تمرینهای فصل نوزده
- ماشینحسابی بنویسید که هر چه کاربر تایپ کند، هرگز خراب نشود.
- تابع
safe_divide(a, b)را بنویسید که به جای خطا،Noneبرگرداند. - نام فایلی را از کاربر بگیرید و اگر وجود نداشت، پیام روشنی بدهید.
- تابع
read_age()را بنویسید که تا وقتی جواب عدد صحیح بین ۱ تا ۱۲۰ نشده بپرسد. - تابعی بنویسید که اگر لیست خالی بگیرد،
ValueErrorبیندازد. - هنگام خواندن دیکشنری
KeyErrorرا بگیرید و آن راه حل را باdict.get()مقایسه کنید. - به یکی از برنامههای قبلیتان
loggingاضافه کنید و گزارش را در فایل بنویسید. - فرق
assertوraiseرا با یک مثال از هر کدام توضیح دهید.