8. خطاها و استثناها¶
تا اینجا پیامهای خطا فقط به صورت کوتاه ذکر شدهاند، اما اگر مثالها را امتحان کرده باشید احتمالاً برخی از آنها را دیدهاید. حداقل دو نوع متمایز از خطا وجود دارد: خطاهای دستوری «syntax errors» و استثناها «exceptions».
8.1. خطاهای دستوری¶
خطاهای دستوری، که بهعنوان خطاهای تجزیهگر «parsing errors» نیز شناخته میشوند، شاید رایجترین نوع خطاهایی باشند که هنگام یادگیری پایتون با آنها مواجه میشوید:
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^^^^^
SyntaxError: invalid syntax
تجزیهگر «parser» خط مشکلدار را تکرار میکند و فلشهای کوچکی را نشان میدهد که به جایی اشاره دارند که خطا در آن شناسایی شده است.
توجه داشته باشید که این همیشه همان جایی نیست که باید اصلاح شود.
در این مثال، خطا در تابع print() شناسایی شده است، زیرا دو نقطه (':') درست قبل از آن وجود ندارد.
نام فایل (در مثال ما <stdin>) و شماره خط چاپ می شوند تا اگر ورودی از یک فایل آمده باشد، بدانید باید کجا را بررسی کنید.
8.2. استثنا ها¶
حتی اگر یک دستور یا عبارت از نظر دستوری «syntactically» درست باشد، ممکن است هنگام اجرای آن خطایی رخ دهد. خطاهایی که هنگام اجرا شناسایی میشوند «استثنا» «exceptions» نام دارند و الزاماً باعث توقف برنامه نمیشوند؛ بهزودی خواهید آموخت چگونه آنها را در برنامههای پایتون مدیریت کنید. با این حال، بیشتر استثناها در برنامهها مدیریت نمیشوند و به پیامهای خطا ختم میشوند، مانند چیزی که در اینجا نمایش داده شده است:
>>> 10 * (1/0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
10 * (1/0)
~^~
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
4 + spam*3
^^^^
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
'2' + 2
~~~~^~~
TypeError: can only concatenate str (not "int") to str
آخرین خط از پیام خطا نشان می دهد که چه اتفاقی افتاده است. استثناها دارای انواع مختلفی هستند و نوع آنها بهعنوان بخشی از پیام چاپ می شود: انواع موجود در مثال عبارتند از ZeroDivisionError، NameError و TypeError. رشته ای که بهعنوان نوع استثنا چاپ می شود، نام استثنای داخلی است که رخ داده است. این موضوع برای تمام استثناهای داخلی صدق می کند، اما برای استثناهای تعریف شده توسط کاربر لزوماً صحیح نیست (گرچه این یک قاعدهٔ مفید محسوب میشود). نام های استثنای استاندارد شناسه های داخلی هستند (نه کلمات کلیدواژهای رزرو شده).
بخش باقیماندهٔ خط، جزئیاتی را بسته به نوع استثنا «exception» و علت وقوع آن ارائه میدهد.
بخش قبلی پیام خطا، زمینهای را نشان میدهد که در آن استثنا رخ داده است، بهصورت یک ردگیری پشته «stack traceback». بهطور کلی، این بخش شامل ردگیری پشتهای است که خطوطی از کد منبع را فهرست میکند؛ با این حال، خطوطی که از ورودی استاندارد «standard input» خوانده شدهاند در آن نمایش داده نمیشوند.
Built-in Exceptions فهرستی از استثناهای داخلی و معانی آن ها را ارائه می دهد.
8.3. مدیریت استثناها¶
امکان نوشتن برنامههایی وجود دارد که برخی از استثناها را مدیریت میکنند. به مثال زیر نگاه کنید که از کاربر ورودی میخواهد تا زمانی که یک عدد صحیح معتبر وارد شود، اما به کاربر اجازه میدهد برنامه را قطع کند (با فشردن Control-C یا هر روشی که سیستمعامل پشتیبانی میکند)؛ توجه داشته باشید که قطع برنامه توسط کاربر با پرتاب استثنای KeyboardInterrupt مشخص میشود.
>>> while True:
... try:
... x = int(input("Please enter a number: "))
... break
... except ValueError:
... print("Oops! That was no valid number. Try again...")
...
دستور try به شکل زیر عمل میکند.
ابتدا بخش try clause (دستوری که بین کلیدواژه های
tryوexceptقرار دارد) اجرا می شود.اگر هیچ استثنایی رخ ندهد، بخش except نادیده گرفته میشود و اجرای دستور
tryبه پایان میرسد.اگر در طول اجرای بخش
tryاستثنایی رخ دهد، بقیه آن بخش نادیده گرفته میشود. سپس، اگر نوع استثنا با استثنایی که بعد از کلیدواژهexceptآمده مطابقت داشته باشد، بخش except اجرا میشود و پس از آن اجرای برنامه بعد از بلوک try/except ادامه مییابد.اگر استثنایی رخ دهد که با استثنای نامبردهشده در بند except مطابقت نداشته باشد، به دستورهای
tryبیرونی منتقل میشود. اگر هیچ رسیدگیکنندهای (handler) پیدا نشود، آن استثنا یک استثنای رسیدگینشده (unhandled exception) محسوب میشود و اجرای برنامه با نمایش یک پیام خطا متوقف خواهد شد.
یک دستور try میتواند بیش از یک بند except داشته باشد تا برای استثناهای مختلف رسیدگیکنندههای متفاوتی تعریف شود. در هر بار وقوع استثنا، حداکثر یکی از این رسیدگیکنندهها اجرا میشود. هر رسیدگیکننده فقط استثناهایی را مدیریت میکند که در بخش try متناظر با آن رخ دادهاند، نه استثناهایی که در سایر رسیدگیکنندههای همان دستور try ایجاد شوند. همچنین یک بند except میتواند چندین استثنا را مشخص کند، برای مثال:
... except RuntimeError, TypeError, NameError:
... pass
کلاسی که در یک except مشخص میشود، با استثناهایی مطابقت دارد که نمونهای از همان کلاس یا یکی از زیرکلاسهای آن باشند (اما برعکس آن صادق نیست؛ یعنی بند except که یک زیرکلاس را مشخص میکند، با نمونههای کلاس پایهٔ آن مطابقت نخواهد داشت). برای مثال، کد زیر بهترتیب B، C و D را چاپ میکند:
class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for cls in [B, C, D]:
try:
raise cls()
except D:
print("D")
except C:
print("C")
except B:
print("B")
توجه کنید که اگر ترتیب بندهای except برعکس بود (یعنی except B در ابتدا قرار میگرفت)، خروجی B، B، B میشد؛ زیرا نخستین بند except که با استثنا مطابقت داشته باشد، اجرا میشود.
هنگامی که یک استثنا رخ میدهد، ممکن است مقادیر مرتبطی نیز همراه آن باشد که به آنها آرگومانهای استثنا گفته میشود. وجود و نوع این آرگومانها به نوع استثنا بستگی دارد.
بند except میتواند پس از نام استثنا، یک متغیر نیز مشخص کند. این متغیر به نمونهٔ استثنا (exception instance) نسبت داده میشود که معمولاً دارای ویژگی args برای ذخیرهٔ آرگومانهای استثنا است. برای راحتی، انواع داخلی استثنا متد __str__() را طوری تعریف کردهاند که بدون نیاز به دسترسی مستقیم به .args، همهٔ آرگومانها را چاپ کند.:
>>> try:
... raise Exception('spam', 'eggs')
... except Exception as inst:
... print(type(inst)) # نوع استثنا
... print(inst.args) # آرگومانهای ذخیرهشده در .args
... print(inst) # __str__ اجازه میدهد آرگومانها مستقیماً چاپ شوند،
... # اما ممکن است در زیرکلاسهای استثنا بازنویسی شود
... x, y = inst.args # باز کردن آرگومانها
... print('x =', x)
... print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
خروجی متد __str__() استثنا بهعنوان آخرین بخش (بخش «جزئیات») پیام مربوط به استثناهای رسیدگینشده چاپ میشود.
BaseException کلاس پایهٔ مشترک همهٔ استثناها است. یکی از زیرکلاسهای آن، یعنی Exception، کلاس پایهٔ تمام استثناهای غیرمرگبار است. استثناهایی که زیرکلاس Exception نیستند معمولاً مدیریت نمیشوند، زیرا برای نشان دادن این موضوع بهکار میروند که برنامه باید خاتمه یابد. از جملهٔ آنها میتوان به SystemExit که توسط sys.exit() پرتاب میشود و KeyboardInterrupt که هنگام درخواست کاربر برای قطع اجرای برنامه پرتاب میشود، اشاره کرد.
Exception را میتوان بهعنوان یک الگوی کلی (wildcard) برای گرفتن (تقریباً) همهٔ استثناها بهکار برد. با این حال، بهتر است تا حد امکان نوع استثناهایی را که قصد مدیریت آنها را داریم بهطور دقیق مشخص کنیم و اجازه دهیم استثناهای غیرمنتظره به سطوح بالاتر منتقل شوند.
رایجترین الگوی مدیریت Exception این است که ابتدا استثنا چاپ یا ثبت (log) شود و سپس دوباره پرتاب گردد تا فراخواننده نیز بتواند در صورت نیاز آن را مدیریت کند:
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print("OS error:", err)
except ValueError:
print("Could not convert data to an integer.")
except Exception as err:
print(f"Unexpected {err=}, {type(err)=}")
raise
دستور try ... except یک بند else اختیاری نیز دارد که در صورت وجود، باید پس از همهٔ بندهای except قرار گیرد. این بند برای کدی مفید است که تنها در صورتی باید اجرا شود که بخش try هیچ استثنایی پرتاب نکرده باشد. برای مثال:
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except OSError:
print('cannot open', arg)
else:
print(arg, 'has', len(f.readlines()), 'lines')
f.close()
استفاده از بند else بهتر از افزودن کد بیشتر به try است، زیرا از این که بهطور ناخواسته استثنایی که توسط کد محافظتشده با دستور try ... except پرتاب نشده است نیز گرفته شود، جلوگیری میکند.
رسیدگیکنندههای استثنا فقط استثناهایی را که مستقیماً در بخش try رخ میدهند مدیریت نمیکنند، بلکه استثناهایی را که درون توابعی که (حتی بهطور غیرمستقیم) از بخش try فراخوانی شدهاند نیز رخ میدهند، مدیریت میکنند. برای مثال:
>>> def this_fails():
... x = 1/0
...
>>> try:
... this_fails()
... except ZeroDivisionError as err:
... print('Handling run-time error:', err)
...
Handling run-time error: division by zero
8.4. پرتاب استثناها¶
دستور raise به برنامهنویس اجازه میدهد وقوع یک استثنای مشخص را بهصورت اجباری پرتاب کند. برای مثال:
>>> raise NameError('HiThere')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
raise NameError('HiThere')
NameError: HiThere
تنها آرگومان دستور raise مشخص میکند که چه استثنایی باید پرتاب شود. این آرگومان باید یا یک نمونهٔ استثنا (exception instance) باشد یا یک کلاس استثنا (کلاسی که از BaseException مشتق شده باشد، مانند Exception یا یکی از زیرکلاسهای آن). اگر یک کلاس استثنا ارسال شود، پایتون بهطور ضمنی با فراخوانی سازندهٔ آن بدون هیچ آرگومانی، یک نمونه از آن ایجاد میکند:
raise ValueError # کوتاهشده برای # exc must be exception instance or None.'raise ValueError()'
اگر فقط لازم است تشخیص دهید که آیا استثنایی رخ داده است یا نه، اما قصد مدیریت آن را ندارید، میتوانید از شکل سادهتری از دستور raise برای پرتاب مجدد همان استثنا استفاده کنید:
>>> try:
... raise NameError('HiThere')
... except NameError:
... print('An exception flew by!')
... raise
...
An exception flew by!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise NameError('HiThere')
NameError: HiThere
8.5. زنجیرهسازی استثناها¶
اگر درون یک بخش except یک استثنای رسیدگینشده رخ دهد، استثنایی که در حال مدیریت شدن بود به آن متصل میشود و در پیام خطا نیز نمایش داده خواهد شد:
>>> try:
... open("database.sqlite")
... except OSError:
... raise RuntimeError("unable to handle error")
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
open("database.sqlite")
~~~~^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError("unable to handle error")
RuntimeError: unable to handle error
برای نشان دادن این که یک استثنا پیامد مستقیم استثنای دیگری است، دستور raise یک بند اختیاری from را در اختیار میگذارد:
# exc باید نمونهای از یک استثنا یا None باشد.
raise RuntimeError from exc
این قابلیت زمانی مفید است که در حال تبدیل یک استثنا به استثنای دیگری باشید. برای مثال:
>>> def func():
... raise ConnectionError
...
>>> try:
... func()
... except ConnectionError as exc:
... raise RuntimeError('Failed to open database') from exc
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
func()
~~~~^^
File "<stdin>", line 2, in func
ConnectionError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError('Failed to open database') from exc
RuntimeError: Failed to open database
همچنین میتوانید با استفاده از الگوی from None زنجیرهسازی خودکار استثناها را غیرفعال کنید:
>>> try:
... open('database.sqlite')
... except OSError:
... raise RuntimeError from None
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError from None
RuntimeError
برای اطلاعات بیشتر دربارهٔ سازوکار زنجیرهسازی استثناها، به Built-in Exceptions مراجعه کنید.
8.6. استثناهای تعریف شده توسط کاربر¶
برنامهها میتوانند با ایجاد یک کلاس استثنای جدید، استثناهای اختصاصی خود را تعریف کنند (برای آشنایی بیشتر با کلاسهای پایتون، به کلاسها مراجعه کنید). معمولاً استثناها باید، بهصورت مستقیم یا غیرمستقیم، از کلاس Exception مشتق شوند.
کلاسهای استثنا میتوانند هر کاری را انجام دهند که سایر کلاسها قادر به انجام آن هستند، اما معمولاً ساده نگه داشته میشوند و اغلب تنها چند ویژگی (attribute) در اختیار میگذارند تا مدیریتکنندههای استثنا بتوانند اطلاعات مربوط به خطا را از آنها استخراج کنند.
نام بیشتر استثناها به «Error» ختم میشود؛ مشابه شیوهٔ نامگذاری استثناهای استاندارد.
بسیاری از ماژولهای استاندارد، استثناهای اختصاصی خود را برای گزارش خطاهایی که ممکن است در توابعشان رخ دهد تعریف میکنند.
8.7. تعریف عملیات پاکسازی¶
دستور try یک بخش اختیاری دیگر نیز دارد که برای تعریف عملیات پاکسازیای در نظر گرفته شده است که باید در هر شرایطی اجرا شوند. برای مثال:
>>> try:
... raise KeyboardInterrupt
... finally:
... print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise KeyboardInterrupt
KeyboardInterrupt
اگر بخش finally وجود داشته باشد، بخش finally بهعنوان آخرین مرحله، پیش از پایان یافتن دستور try اجرا میشود. بخش finally صرفنظر از اینکه بخش try استثنایی ایجاد کرده باشد یا نه، اجرا خواهد شد. نکات زیر حالتهای پیچیدهتر هنگام وقوع استثنا را توضیح میدهند:
اگر هنگام اجرای بخش
tryاستثنایی رخ دهد، ممکن است توسط یک بخشexceptمدیریت شود. اگر استثنا توسط بخشexceptمدیریت نشود، پس از اجرای بخشfinallyدوباره پرتاب (re-raise) خواهد شد.ممکن است هنگام اجرای بخش
exceptیاelseنیز استثنایی رخ دهد. در این حالت نیز، پس از اجرای بخشfinally، استثنا دوباره پرتاب خواهد شد.اگر بخش
finallyشامل یکی از دستوراتbreak،continueیاreturnباشد، استثناها دوباره پرتاب نخواهند شد. این رفتار ممکن است باعث سردرگمی شود و به همین دلیل استفاده از آن توصیه نمیشود. از نسخهٔ ۳٫۱۴، کامپایلر برای این حالت یکSyntaxWarningصادر میکند (به PEP 765 مراجعه کنید).اگر دستور
tryبه یکی از دستوراتbreak،continueیاreturnبرسد، بخشfinallyدرست پیش از اجرای دستورbreak،continueیاreturnاجرا خواهد شد.اگر بخش
finallyشامل یک دستورreturnباشد، مقدار بازگشتی همان مقداری خواهد بود که در دستورreturnداخل بخشfinallyمشخص شده است، نه مقداری که در دستورreturnداخل بخشtryتعیین شده بود. این رفتار ممکن است باعث سردرگمی شود و به همین دلیل استفاده از آن توصیه نمیشود. از نسخهٔ ۳٫۱۴، کامپایلر برای این حالت یکSyntaxWarningصادر میکند (به PEP 765 مراجعه کنید).
برای مثال:
>>> def bool_return():
... try:
... return True
... finally:
... return False
...
>>> bool_return()
False
یک مثال پیچیدهتر:
>>> def divide(x, y):
... try:
... result = x / y
... except ZeroDivisionError:
... print("division by zero!")
... else:
... print("result is", result)
... finally:
... print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
divide("2", "1")
~~~~~~^^^^^^^^^^
File "<stdin>", line 3, in divide
result = x / y
~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'
همانطور که میبینید، بخش finally در هر صورت اجرا میشود. استثنای TypeError که در اثر تقسیم دو رشته پرتاب شده است، توسط بخش except مدیریت نمیشود و بنابراین پس از اجرای بخش finally دوباره پرتاب میشود.
در برنامههای واقعی، بخش finally برای آزاد کردن منابع خارجی (مانند فایلها یا اتصالهای شبکه) مفید است، صرفنظر از اینکه استفاده از آن منبع با موفقیت انجام شده باشد یا خیر.
8.8. عملیات پاکسازی از پیش تعریفشده¶
برخی اشیا عملیات پاکسازی استانداردی را تعریف میکنند که باید زمانی انجام شوند که آن شیء دیگر موردنیاز نباشد، صرفنظر از اینکه عملیات مربوط به آن موفق بوده یا شکست خورده باشد. به مثال زیر توجه کنید که تلاش میکند فایلی را باز کند و محتوای آن را روی صفحه نمایش دهد.:
for line in open("myfile.txt"):
print(line, end="")
مشکل این کد این است که پس از پایان اجرای این بخش، فایل را برای مدت نامشخصی باز نگه میدارد. در اسکریپتهای ساده این موضوع معمولاً مشکلی ایجاد نمیکند، اما در برنامههای بزرگتر میتواند دردسرساز باشد. دستور with این امکان را فراهم میکند که از اشیایی مانند فایلها بهگونهای استفاده شود که همیشه بهموقع و بهدرستی پاکسازی شوند.:
with open("myfile.txt") as f:
for line in f:
print(line, end="")
پس از اجرای این دستور، فایل f همیشه بسته میشود، حتی اگر هنگام پردازش خطوط فایل مشکلی رخ داده باشد. اشیایی که مانند فایلها عملیات پاکسازی از پیش تعریفشده دارند، این موضوع را در مستندات خود ذکر میکنند.
8.10. افزودن یادداشت به استثناها¶
وقتی یک استثنا برای پرتاب شدن پرتاب میشود، معمولاً با اطلاعاتی مقداردهی اولیه میشود که خطای رخداده را توصیف میکنند. بااینحال، گاهی مفید است که پس از مدیریت (catch) شدن استثنا نیز اطلاعات بیشتری به آن افزوده شود. برای این منظور، استثناها متدی به نام add_note(note) دارند که یک رشته را دریافت کرده و آن را به فهرست یادداشتهای استثنا اضافه میکند. نمایش استاندارد traceback همهٔ این یادداشتها را، به همان ترتیبی که اضافه شدهاند، پس از اطلاعات استثنا نمایش میدهد.:
>>> try:
... raise TypeError('bad type')
... except Exception as e:
... e.add_note('Add some information')
... e.add_note('Add some more information')
... raise
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise TypeError('bad type')
TypeError: bad type
Add some information
Add some more information
>>>
برای مثال، هنگام جمعآوری چند استثنا در یک گروه استثنا، ممکن است بخواهیم اطلاعات زمینهای (context) را به هر خطا اضافه کنیم. در مثال زیر، هر استثنای موجود در گروه یادداشتی دارد که مشخص میکند آن خطا در چه زمانی رخ داده است.:
>>> def f():
... raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
... try:
... f()
... except Exception as e:
... e.add_note(f'Happened in Iteration {i+1}')
... excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| raise ExceptionGroup('We have some problems', excs)
| ExceptionGroup: We have some problems (3 sub-exceptions)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 1
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 2
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 3
+------------------------------------
>>>