Python پایتون ۳.۱۴
فصل ۸

خطاها و استثناها (Errors and Exceptions)

📖 ۱۶ دقیقه 🎯 متوسط 🔄 ۲۰۲۶

تا کنون پیام‌های خطا بیشتر از اینکه ذکر شوند، نادیده گرفته شده‌اند، اما اگر مثال‌ها را امتحان کرده باشید، احتمالاً برخی از آن‌ها را دیده‌اید. دو نوع قابل‌تشخیص از خطاها وجود دارد: خطاهای نحوی (syntax errors) و استثناها (exceptions).

۸.۱. خطاهای نحوی (Syntax Errors)

خطاهای نحوی، که به‌عنوان خطاهای تجزیه (parsing errors) نیز شناخته می‌شوند، شاید رایج‌ترین نوع شکایتی باشند که در حین یادگیری پایتون با آن‌ها مواجه می‌شوید:

>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
                   ^^^^^
SyntaxError: invalid syntax

تجزیه‌گر (parser) خط متخلف را تکرار می‌کند و فلش‌های کوچکی را نشان می‌دهد که به محلی که خطا تشخیص داده شده است، اشاره می‌کنند. توجه داشته باشید که این همیشه محلی نیست که باید اصلاح شود. در مثال، خطا در تابع print() تشخیص داده می‌شود، زیرا یک دونقطه (':') درست قبل از آن جا افتاده است.

نام فایل (<stdin> در مثال ما) و شمارهٔ خط چاپ می‌شوند تا بدانید در صورتی که ورودی از یک فایل آمده است، کجا را نگاه کنید.

۸.۲. استثناها (Exceptions)

حتی اگر یک دستور یا عبارت از نظر نحوی درست باشد، ممکن است هنگام تلاش برای اجرای آن، خطایی ایجاد کند. خطاهایی که در حین اجرا تشخیص داده می‌شوند، استثنا (exception) نامیده می‌شوند و به‌طور نامشروط کشنده (fatal) نیستند: به‌زودی یاد خواهید گرفت که چگونه آن‌ها را در برنامه‌های پایتون مدیریت کنید. با این حال، بیشتر استثناها توسط برنامه‌ها مدیریت نمی‌شوند و در نتیجه پیام‌های خطایی مانند زیر نشان داده می‌شوند:

>>> 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. رشته‌ای که به‌عنوان نوع استثنا چاپ می‌شود، نام استثنای توکاری (built-in exception) است که رخ داده است. این برای تمام استثناهای توکار صادق است، اما لزوماً برای استثناهای تعریف‌شده توسط کاربر (user-defined exceptions) صادق نیست (اگرچه یک قرارداد مفید است). نام‌های استثنای استاندارد، شناسه‌های توکار (built-in identifiers) هستند (نه کلیدواژه‌های رزرو شده).

بقیهٔ خط، جزئیاتی را بر اساس نوع استثنا و علت آن ارائه می‌دهد.

بخش قبلی پیام خطا، زمینه‌ای را که استثنا در آن رخ داده است، در قالب یک stack traceback نشان می‌دهد. به‌طور کلی، این شامل یک stack traceback است که خطوط منبع را فهرست می‌کند؛ با این حال، خطوط خوانده‌شده از ورودی استاندارد (standard input) را نمایش نمی‌دهد.

Built-in Exceptions استثناهای توکار و معانی آن‌ها را فهرست می‌کند.

۸.۳. مدیریت استثناها (Handling Exceptions)

امکان نوشتن برنامه‌هایی وجود دارد که استثناهای انتخاب‌شده را مدیریت کنند. به مثال زیر نگاه کنید، که از کاربر تا زمانی که یک عدد صحیح معتبر وارد شود، ورودی می‌خواهد، اما به کاربر اجازه می‌دهد برنامه را قطع کند (با استفاده از 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 clause نادیده گرفته می‌شود و اجرای دستور try به پایان می‌رسد.
  • اگر در حین اجرای try clause استثنایی رخ دهد، بقیهٔ clause نادیده گرفته می‌شود. سپس، اگر نوع آن با استثنای نام‌برده‌شده بعد از کلیدواژهٔ except مطابقت داشته باشد، except clause اجرا می‌شود و سپس اجرا بعد از بلوک try/except ادامه می‌یابد.
  • اگر استثنایی رخ دهد که با استثنای نام‌برده‌شده در except clause مطابقت نداشته باشد، به دستورات try بیرونی منتقل می‌شود؛ اگر هیچ مدیریت‌کننده‌ای (handler) پیدا نشود، یک استثنای مدیریت‌نشده (unhandled exception) است و اجرا با یک پیام خطا متوقف می‌شود.

یک دستور try ممکن است بیش از یک except clause داشته باشد، تا مدیریت‌کننده‌هایی برای استثناهای مختلف مشخص شود. حداکثر یک مدیریت‌کننده اجرا خواهد شد. مدیریت‌کننده‌ها فقط استثناهایی را مدیریت می‌کنند که در try clause مربوطه رخ می‌دهند، نه در سایر مدیریت‌کننده‌های همان دستور try. یک except clause ممکن است چندین استثنا را نام ببرد، برای مثال:

... except (RuntimeError, TypeError, NameError):
...     pass

یک کلاس در یک except clause با استثناهایی مطابقت دارد که نمونه‌هایی از خود کلاس یا یکی از کلاس‌های مشتق‌شده (derived classes) آن باشند (اما نه برعکس — یک except clause که یک کلاس مشتق‌شده را فهرست می‌کند، با نمونه‌های کلاس‌های پایه (base classes) آن مطابقت ندارد). برای مثال، کد زیر به ترتیب 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")
...
B
C
D

توجه داشته باشید که اگر except clauses برعکس می‌شدند (با except B اول)، B، B، B چاپ می‌شد — اولین except clause منطبق فعال می‌شود.

هنگامی که یک استثنا رخ می‌دهد، ممکن است مقادیر مرتبطی داشته باشد، که به‌عنوان آرگومان‌های (arguments) استثنا نیز شناخته می‌شوند. وجود و انواع آرگومان‌ها به نوع استثنا بستگی دارد.

except clause ممکن است یک متغیر بعد از نام استثنا مشخص کند. این متغیر به نمونهٔ استثنا (exception instance) متصل می‌شود که معمولاً یک attribute به نام args دارد که آرگومان‌ها را ذخیره می‌کند. برای راحتی، انواع استثنای توکار، __str__() را تعریف می‌کنند تا تمام آرگومان‌ها را بدون دسترسی صریح به .args چاپ کنند.

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))      # the exception type
...     print(inst.args)       # arguments stored in .args
...     print(inst)            # __str__ allows args to be printed directly,
...                            # but may be overridden in exception subclasses
...     x, y = inst.args       # unpack args
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

خروجی __str__() استثنا به‌عنوان آخرین بخش ('جزئیات') پیام برای استثناهای مدیریت‌نشده چاپ می‌شود.

BaseException کلاس پایهٔ مشترک تمام استثناها است. یکی از زیرکلاس‌های آن، Exception، کلاس پایهٔ تمام استثناهای غیرکشنده (non-fatal) است. استثناهایی که زیرکلاس Exception نیستند، معمولاً مدیریت نمی‌شوند، زیرا برای نشان دادن اینکه برنامه باید خاتمه یابد، استفاده می‌شوند. آن‌ها شامل SystemExit هستند که توسط sys.exit() ایجاد می‌شود و KeyboardInterrupt که زمانی ایجاد می‌شود که کاربر بخواهد برنامه را قطع کند.

از Exception می‌توان به‌عنوان یک wildcard استفاده کرد که (تقریباً) همه چیز را می‌گیرد. با این حال، بهتر است تا حد امکان در مورد انواع استثناهایی که قصد مدیریت آن‌ها را داریم، مشخص باشیم و اجازه دهیم هر استثنای غیرمنتظره‌ای به بالا منتشر شود.

رایج‌ترین الگو برای مدیریت Exception این است که استثنا را چاپ یا لاگ (log) کرده و سپس دوباره آن را ایجاد کنیم (تا به فراخواننده (caller) اجازه دهیم استثنا را نیز مدیریت کند):

>>> 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 clause اختیاری دارد که در صورت وجود، باید بعد از تمام except clauses قرار گیرد. این clause برای کدی مفید است که باید در صورتی اجرا شود که try clause استثنایی ایجاد نکند. برای مثال:

>>> 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 clause بهتر از اضافه کردن کد اضافی به try clause است زیرا از گیر انداختن تصادفی استثنایی که توسط کدی که توسط دستور try ... except محافظت می‌شود، ایجاد نشده است، جلوگیری می‌کند.

مدیریت‌کننده‌های استثنا نه‌تنها استثناهایی را که بلافاصله در try clause رخ می‌دهند، مدیریت می‌کنند، بلکه آن‌هایی را که در توابع فراخوانی‌شده (حتی به‌طور غیرمستقیم) در try clause رخ می‌دهند، نیز مدیریت می‌کنند. برای مثال:

>>> 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

۸.۴. ایجاد استثناها (Raising Exceptions)

دستور 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 یا یکی از زیرکلاس‌های آن). اگر یک کلاس استثنا ارسال شود، به‌طور ضمنی با فراخوانی سازنده (constructor) آن بدون آرگومان، نمونه‌سازی (instantiate) می‌شود:

>>> raise ValueError  # shorthand for 'raise ValueError()'

اگر نیاز دارید تعیین کنید که آیا استثنایی ایجاد شده است اما قصد مدیریت آن را ندارید، یک شکل ساده‌تر از دستور raise به شما امکان می‌دهد استثنا را دوباره ایجاد کنید (re-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

۸.۵. زنجیره‌سازی استثناها (Exception Chaining)

اگر یک استثنای مدیریت‌نشده در داخل یک بخش 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 clause اختیاری را مجاز می‌داند:

# exc must be exception instance or None.
raise RuntimeError from exc

این می‌تواند زمانی که در حال تبدیل استثناها (transforming exceptions) هستید مفید باشد. برای مثال:

>>> 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
    raise ConnectionError
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

همچنین امکان غیرفعال کردن زنجیره‌سازی خودکار استثنا با استفاده از idiom 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 مراجعه کنید.

۸.۶. استثناهای تعریف‌شده توسط کاربر (User-defined Exceptions)

برنامه‌ها می‌توانند با ایجاد یک کلاس استثنای جدید، استثناهای خود را نام‌گذاری کنند (برای اطلاعات بیشتر در مورد کلاس‌های پایتون به Classes مراجعه کنید). استثناها باید به‌طور معمول، به‌طور مستقیم یا غیرمستقیم، از کلاس Exception مشتق شوند.

کلاس‌های استثنا را می‌توان تعریف کرد که هر کاری که هر کلاس دیگری می‌تواند انجام دهد، انجام دهند، اما معمولاً ساده نگه داشته می‌شوند، اغلب فقط تعدادی attribute ارائه می‌دهند که به مدیریت‌کننده‌های استثنا اجازه می‌دهد اطلاعاتی در مورد خطا استخراج کنند.

بیشتر استثناها با نام‌هایی تعریف می‌شوند که به "Error" ختم می‌شوند، مشابه نام‌گذاری استثناهای استاندارد.

بسیاری از ماژول‌های استاندارد، استثناهای خود را برای گزارش خطاهایی که ممکن است در توابعی که تعریف می‌کنند رخ دهد، تعریف می‌کنند.

۸.۷. تعریف اقدامات پاک‌سازی (Defining Clean-up Actions)

دستور try یک clause اختیاری دیگر دارد که برای تعریف اقدامات پاک‌سازی (clean-up actions) در نظر گرفته شده است که باید تحت تمام شرایط اجرا شوند. برای مثال:

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise KeyboardInterrupt
KeyboardInterrupt

اگر یک finally clause وجود داشته باشد، این clause به‌عنوان آخرین کار قبل از تکمیل دستور try اجرا خواهد شد. finally clause صرف‌نظر از اینکه دستور try استثنایی ایجاد کند یا نه، اجرا می‌شود. نکات زیر موارد پیچیده‌تری را هنگامی که استثنایی رخ می‌دهد، بحث می‌کنند:

  • اگر در حین اجرای try clause استثنایی رخ دهد، ممکن است استثنا توسط یک except clause مدیریت شود. اگر استثنا توسط یک except clause مدیریت نشود، پس از اجرای finally clause دوباره ایجاد می‌شود.
  • ممکن است در حین اجرای یک except یا else clause استثنایی رخ دهد. باز هم، پس از اجرای finally clause استثنا دوباره ایجاد می‌شود.
  • اگر finally clause یک دستور break، continue یا return را اجرا کند، استثناها دوباره ایجاد نمی‌شوند. این می‌تواند گیج‌کننده باشد و بنابراین توصیه نمی‌شود. از نسخهٔ ۳.۱۴، کامپایلر برای آن یک SyntaxWarning صادر می‌کند (به PEP 765 مراجعه کنید).
  • اگر دستور try به یک دستور break، continue یا return برسد، finally clause درست قبل از اجرای دستور break، continue یا return اجرا خواهد شد.
  • اگر یک finally clause شامل یک دستور return باشد، مقدار برگشتی، مقدار حاصل از دستور return finally clause خواهد بود، نه مقدار حاصل از دستور return try clause. این می‌تواند گیج‌کننده باشد و بنابراین توصیه نمی‌شود. از نسخهٔ ۳.۱۴، کامپایلر برای آن یک 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 clause در هر صورت اجرا می‌شود. TypeError ایجادشده توسط تقسیم دو رشته توسط except clause مدیریت نمی‌شود و بنابراین پس از اجرای finally clause دوباره ایجاد می‌شود.

در برنامه‌های دنیای واقعی، finally clause برای آزادسازی منابع خارجی (مانند فایل‌ها یا اتصالات شبکه)، صرف‌نظر از اینکه استفاده از منبع موفق بوده است یا خیر، مفید است.

۸.۸. اقدامات پاک‌سازی از پیش تعریف‌شده (Predefined Clean-up Actions)

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

>>> for line in open("myfile.txt"):
...     print(line, end="")
...

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

>>> with open("myfile.txt") as f:
...     for line in f:
...         print(line, end="")
...

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

۸.۹. ایجاد و مدیریت چندین استثنای نامرتبط (Raising and Handling Multiple Unrelated Exceptions)

موقعیت‌هایی وجود دارد که لازم است چندین استثنا که رخ داده‌اند، گزارش شوند. این اغلب در چارچوب‌های هم‌روندی (concurrency frameworks) اتفاق می‌افتد، زمانی که ممکن است چندین task به‌طور موازی شکست خورده باشند، اما موارد استفادهٔ دیگری نیز وجود دارد که در آن‌ها مطلوب است که اجرا ادامه یابد و چندین خطا جمع‌آوری شوند به جای اینکه اولین استثنا ایجاد شود.

ExceptionGroup توکار، لیستی از نمونه‌های استثنا را می‌پیچد تا بتوانند با هم ایجاد شوند. خودش یک استثنا است، بنابراین می‌توان مانند هر استثنای دیگری آن را گرفت.

>>> def f():
...     excs = [OSError('error 1'), SystemError('error 2')]
...     raise ExceptionGroup('there were problems', excs)
...
>>> f()
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     f()
  |     ~^^
  |   File "<stdin>", line 3, in f
  |     raise ExceptionGroup('there were problems', excs)
  | ExceptionGroup: there were problems (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | OSError: error 1
    +---------------- 2 ----------------
    | SystemError: error 2
    +------------------------------------
>>> try:
...     f()
... except Exception as e:
...     print(f'caught {type(e)}: {e}')
...
caught <class 'ExceptionGroup'>: there were problems (2 sub-exceptions)

با استفاده از except* به جای except، می‌توانیم فقط استثناهای موجود در گروه را که با یک نوع خاص مطابقت دارند، به‌طور انتخابی مدیریت کنیم. در مثال زیر، که یک گروه استثنای تو در تو (nested) را نشان می‌دهد، هر except* clause استثناهای یک نوع خاص را از گروه استخراج می‌کند در حالی که اجازه می‌دهد سایر استثناها به clauses دیگر و در نهایت برای ایجاد مجدد منتشر شوند.

>>> def f():
...     raise ExceptionGroup(
...         "group1",
...         [
...             OSError(1),
...             SystemError(2),
...             ExceptionGroup(
...                 "group2",
...                 [
...                     OSError(3),
...                     RecursionError(4)
...                 ]
...             )
...         ]
...     )
...
>>> try:
...     f()
... except* OSError as e:
...     print("There were OSErrors")
... except* SystemError as e:
...     print("There were SystemErrors")
...
There were OSErrors
There were SystemErrors
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 2, in <module>
  |     f()
  |     ~^^
  |   File "<stdin>", line 2, in f
  |     raise ExceptionGroup(
  |         ... <12 lines> ...
  |     )
  | ExceptionGroup: group1 (1 sub-exception)
  +-+---------------- 1 ----------------
    | ExceptionGroup: group2 (1 sub-exception)
    +-+---------------- 1 ----------------
      | RecursionError: 4
      +------------------------------------

توجه داشته باشید که استثناهای تو در تو در یک گروه استثنا باید نمونه (instance) باشند، نه نوع (type). این به این دلیل است که در عمل، استثناها معمولاً آن‌هایی هستند که قبلاً توسط برنامه ایجاد و گرفته شده‌اند، در الگوی زیر:

>>> excs = []
>>> for test in tests:
...     try:
...         test.run()
...     except Exception as e:
...         excs.append(e)
...
>>> if excs:
...     raise ExceptionGroup("Test Failures", excs)
...

۸.۱۰. غنی‌سازی استثناها با یادداشت‌ها (Enriching Exceptions with Notes)

هنگامی که یک استثنا برای ایجاد شدن ساخته می‌شود، معمولاً با اطلاعاتی که خطای رخ‌داده را توصیف می‌کند، مقداردهی اولیه (initialize) می‌شود. مواردی وجود دارد که افزودن اطلاعات پس از گرفتن استثنا مفید است. برای این منظور، استثناها دارای متد 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 information) را برای خطاهای جداگانه اضافه کنیم. در مثال زیر، هر استثنا در گروه یک یادداشت دارد که نشان می‌دهد این خطا چه زمانی رخ داده است.

>>> 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
    +------------------------------------