منابع ترجمه · The Python Tutorial · فصل ۸
خطاها و استثناها (آموزش رسمی پایتون، فصل ۸)
منبع اصلی: The Python Tutorial, Chapter 8: Errors and Exceptions — Python Software Foundation
💡 این صفحه مثل یک نوتبوک Jupyter است: متغیر و تابعی که در یک سلول تعریف شود، در سلولهای بعدی همین صفحه در دسترس است. سلولها را به ترتیب اجرا کنید، یا با «⏮ تا اینجا» همهٔ سلولهای قبلیِ اجرانشده را یکجا اجرا کنید.
تا اینجا پیامهای خطا بیش از اشارهای گذرا نبودهاند، اما اگر مثالها را امتحان کرده باشید، احتمالاً چند تا از آنها را دیدهاید. دستکم دو نوع خطای قابلتمایز وجود دارد: خطاهای نحوی (syntax errors) و استثناها (exceptions).
۸٫۱. خطاهای نحوی
خطاهای نحوی، که با نام خطاهای تجزیه (parsing errors) هم شناخته میشوند، شاید رایجترین نوع شکایتی باشند که هنگام یادگیری پایتون دریافت میکنید:
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^^^^^
SyntaxError: invalid syntax
تجزیهگر خط خطادار را تکرار میکند و فلشهای کوچکی نشان میدهد که به جایی اشاره میکنند که خطا در آن تشخیص داده شده است. توجه کنید که این همیشه جایی نیست که باید اصلاح شود. در مثال، خطا در تابع print() تشخیص داده شده است، چون یک دونقطه (':') درست پیش از آن کم است.
نام فایل (<stdin> در مثال ما) و شمارهٔ خط چاپ میشوند تا در صورتی که ورودی از یک فایل آمده باشد بدانید کجا را نگاه کنید.
۸٫۲. استثناها
حتی اگر یک دستور یا عبارت از نظر نحوی درست باشد، ممکن است هنگام تلاش برای اجرای آن خطا ایجاد کند. خطاهایی که هنگام اجرا تشخیص داده میشوند استثنا نامیده میشوند و بیقید و شرط مرگبار نیستند: بهزودی یاد خواهید گرفت چگونه آنها را در برنامههای پایتون مدیریت کنید. با این حال، بیشتر استثناها توسط برنامهها مدیریت نمیشوند و به پیامهای خطا میانجامند، همانطور که اینجا نشان داده شده است:
>>> 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 هستند. رشتهای که بهعنوان نوع استثنا چاپ میشود نام استثنای درونساختی است که رخ داده است. این برای همهٔ استثناهای درونساخت درست است، اما لازم نیست برای استثناهای تعریفشده توسط کاربر درست باشد (هرچند قراردادی مفید است). نامهای استثنای استاندارد شناسههای درونساخت هستند (نه کلیدواژههای رزروشده).
بقیهٔ خط جزئیاتی بر اساس نوع استثنا و آنچه موجب آن شده فراهم میکند.
بخش قبلی پیام خطا زمینهای را که استثنا در آن رخ داده، به شکل یک ردیاب پشته (stack traceback)، نشان میدهد. بهطور کلی شامل یک ردیاب پشته است که خطوط منبع را فهرست میکند؛ با این حال خطوطی را که از ورودی استاندارد خوانده شدهاند نمایش نمیدهد.
استثناهای درونساخت استثناهای درونساخت و معنای آنها را فهرست میکند.
۸٫۳. مدیریت استثناها
میتوان برنامههایی نوشت که استثناهای انتخابشده را مدیریت کنند. مثال زیر را ببینید که از کاربر ورودی میخواهد تا زمانی که یک عدد صحیح معتبر وارد شود، اما به کاربر اجازه میدهد برنامه را قطع کند (با 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 (دستور یا دستورهای بین کلیدواژههای
tryوexcept) اجرا میشود. -
اگر هیچ استثنایی رخ ندهد، بند except نادیده گرفته میشود و اجرای دستور
tryپایان مییابد. -
اگر هنگام اجرای بند
tryاستثنایی رخ دهد، بقیهٔ بند نادیده گرفته میشود. سپس، اگر نوع آن با استثنای نامبردهشده بعد از کلیدواژهٔexceptمطابقت داشته باشد، بند except اجرا میشود و سپس اجرا بعد از بلوک try/except ادامه مییابد. -
اگر استثنایی رخ دهد که با استثنای نامبردهشده در بند except مطابقت نداشته باشد، به دستورهای
tryبیرونیتر ارسال میشود؛ اگر هیچ مدیریتکنندهای پیدا نشود، یک استثنای مدیریتنشده است و اجرا با یک پیام خطا متوقف میشود.
یک دستور 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 میتواند بعد از نام استثنا یک متغیر مشخص کند. متغیر به نمونهٔ استثنا متصل میشود که معمولاً ویژگی 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، کلاس پایهٔ همهٔ استثناهای غیرمرگبار است. استثناهایی که زیرکلاس 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
۸٫۴. پرتاب استثناها
دستور raise به برنامهنویس اجازه میدهد رخ دادن یک استثنای مشخص را اجبار کند. برای مثال:
>>> raise NameError('HiThere')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
raise NameError('HiThere')
NameError: HiThere
تنها آرگومان raise استثنایی را که باید پرتاب شود مشخص میکند. این باید یا یک نمونهٔ استثنا یا یک کلاس استثنا باشد (کلاسی که از BaseException مشتق میشود، مانند Exception یا یکی از زیرکلاسهای آن). اگر یک کلاس استثنا ارسال شود، بهطور ضمنی با فراخوانی سازندهاش بدون آرگومان نمونهسازی میشود:
raise ValueError # shorthand for '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
۸٫۵. زنجیرهسازی استثنا
اگر یک استثنای مدیریتنشده درون بخش 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 must be exception instance or 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
برای اطلاعات بیشتر دربارهٔ سازوکار زنجیرهسازی، استثناهای درونساخت را ببینید.
۸٫۶. استثناهای تعریفشده توسط کاربر
برنامهها میتوانند با ساختن یک کلاس استثنای جدید، استثناهای خودشان را نامگذاری کنند (برای اطلاعات بیشتر دربارهٔ کلاسهای پایتون به کلاسها نگاه کنید). استثناها معمولاً باید از کلاس Exception مشتق شوند، چه مستقیم و چه غیرمستقیم.
میتوان کلاسهای استثنایی تعریف کرد که هر کاری که هر کلاس دیگری میتواند انجام دهد را انجام دهند، اما معمولاً ساده نگه داشته میشوند و اغلب فقط تعدادی ویژگی ارائه میدهند که به مدیریتکنندههای استثنا اجازه میدهد اطلاعات دربارهٔ خطا را استخراج کنند.
بیشتر استثناها با نامهایی که به «Error» ختم میشوند تعریف میشوند، مشابه نامگذاری استثناهای استاندارد.
بسیاری از ماژولهای استاندارد استثناهای خودشان را تعریف میکنند تا خطاهایی را که ممکن است در توابعی که تعریف میکنند رخ دهد گزارش کنند.
۸٫۷. تعریف اقدامات پاکسازی
دستور try بند اختیاری دیگری دارد که برای تعریف اقدامات پاکسازی (clean-up) در نظر گرفته شده که باید تحت همهٔ شرایط اجرا شوند. برای مثال:
>>> 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دوباره پرتاب میشود. -
ممکن است هنگام اجرای یک بند
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 برای آزاد کردن منابع خارجی (مانند فایلها یا اتصالهای شبکه) مفید است، صرفنظر از اینکه استفاده از منبع موفق بوده یا نه.
۸٫۸. اقدامات پاکسازی از پیش تعریفشده
برخی اشیا اقدامات پاکسازی استانداردی تعریف میکنند که وقتی شیء دیگر لازم نیست انجام شود، صرفنظر از اینکه عملیاتی که از شیء استفاده میکرد موفق بوده یا شکست خورده. مثال زیر را ببینید که سعی میکند یک فایل را باز کند و محتوای آن را روی صفحه چاپ کند.
for line in open("myfile.txt"):
print(line, end="")
مشکل این کد این است که فایل را برای مدتی نامعلوم پس از پایان اجرای این بخش از کد باز میگذارد. این در اسکریپتهای ساده مسئلهای نیست، اما میتواند برای برنامههای بزرگتر مشکل باشد. دستور with به اشیایی مانند فایلها اجازه میدهد به شکلی به کار روند که تضمین میکند همیشه فوراً و بهدرستی پاکسازی میشوند.
with open("myfile.txt") as f:
for line in f:
print(line, end="")
پس از اجرای دستور، فایل f همیشه بسته میشود، حتی اگر هنگام پردازش خطوط مشکلی پیش آمده باشد. اشیایی که، مانند فایلها، اقدامات پاکسازی از پیش تعریفشده ارائه میدهند این را در مستندات خود نشان خواهند داد.
۸٫۹. پرتاب و مدیریت چند استثنای نامرتبط
موقعیتهایی هست که لازم است چندین استثنایی را که رخ دادهاند گزارش کنیم. این اغلب در چارچوبهای همزمانی (concurrency) رخ میدهد، وقتی چند وظیفه ممکن است بهطور موازی شکست خورده باشند، اما موارد استفادهٔ دیگری هم هست که در آنها مطلوب است اجرا ادامه یابد و چند خطا جمعآوری شود، بهجای اینکه اولین استثنا پرتاب شود.
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، میتوانیم بهطور انتخابی فقط استثناهای گروه را که با نوع معینی مطابقت دارند مدیریت کنیم. در مثال زیر، که یک گروه استثنای تودرتو را نشان میدهد، هر بند except* از گروه استثناهایی از یک نوع مشخص را استخراج میکند و اجازه میدهد همهٔ استثناهای دیگر به بندهای دیگر منتشر شوند و در نهایت دوباره پرتاب شوند.
>>> 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
+------------------------------------
>>>
توجه کنید که استثناهای تودرتو در یک گروه استثنا باید نمونه باشند، نه نوع. دلیلش این است که در عمل استثناها معمولاً آنهایی هستند که قبلاً توسط برنامه پرتاب و گرفته شدهاند، بر اساس الگوی زیر:
>>> excs = []
... for test in tests:
... try:
... test.run()
... except Exception as e:
... excs.append(e)
...
>>> if excs:
... raise ExceptionGroup("Test Failures", excs)
...
۸٫۱۰. غنیسازی استثناها با یادداشتها
وقتی استثنایی برای پرتاب شدن ساخته میشود، معمولاً با اطلاعاتی که خطای رخداده را توصیف میکند مقداردهی اولیه میشود. مواردی هست که افزودن اطلاعات پس از گرفتن استثنا مفید است. برای این منظور، استثناها متدی به نام add_note(note) دارند که یک رشته میپذیرد و آن را به فهرست یادداشتهای استثنا اضافه میکند. نمایش استاندارد ردیاب پشته همهٔ یادداشتها را، به ترتیبی که اضافه شدهاند، بعد از استثنا شامل میشود.
>>> 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
>>>
برای مثال، هنگام جمعآوری استثناها در یک گروه استثنا، ممکن است بخواهیم اطلاعات زمینهای را برای خطاهای منفرد اضافه کنیم. در مثال زیر هر استثنای گروه یادداشتی دارد که نشان میدهد این خطا چه زمانی رخ داده است.
>>> 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
+------------------------------------
>>>