8. Lỗi và Ngoại Lệ (Errors and Exceptions)
Cho đến nay, các thông báo lỗi chưa được đề cập nhiều, nhưng nếu bạn đã thử các ví dụ, có lẽ bạn đã gặp phải một số lỗi. Có (ít nhất) hai loại lỗi khác nhau: lỗi cú pháp (syntax errors) và ngoại lệ (exceptions).
8.1. Lỗi Cú Pháp (Syntax Errors)
Lỗi cú pháp, còn được gọi là lỗi phân tích cú pháp (parsing errors), có lẽ là loại lỗi phổ biến nhất khi bạn mới học Python:
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^^^^^
SyntaxError: invalid syntax
Bộ phân tích cú pháp sẽ lặp lại dòng gây lỗi và hiển thị mũi tên ^^^^^
trỏ vào vị trí có lỗi. Lỗi có thể do thiếu một ký tự nào đó trước vị trí được chỉ báo. Trong ví dụ trên, lỗi được phát hiện tại hàm print()
, vì thiếu dấu hai chấm (:
) trước nó.
Tên tệp và số dòng cũng được hiển thị để giúp bạn dễ dàng tìm thấy vị trí lỗi trong trường hợp lỗi đến từ một tập lệnh Python.
8.2. Ngoại Lệ (Exceptions)
Ngay cả khi một câu lệnh hoặc biểu thức có cú pháp hợp lệ, nó vẫn có thể gây lỗi khi thực thi. Những lỗi được phát hiện trong quá trình chạy chương trình được gọi là ngoại lệ (exceptions). Không phải tất cả ngoại lệ đều gây dừng chương trình ngay lập tức, và bạn sẽ sớm học cách xử lý chúng trong Python.
Hầu hết ngoại lệ không được chương trình xử lý và kết quả là một thông báo lỗi như sau:
>>> 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
Dòng cuối cùng của thông báo lỗi cho biết loại ngoại lệ và mô tả lỗi xảy ra. Trong các ví dụ trên, các ngoại lệ bao gồm ZeroDivisionError
, NameError
, và TypeError
. Tên ngoại lệ được in ra chính là tên của ngoại lệ được tích hợp sẵn trong Python.
Phần trước của thông báo lỗi cung cấp ngữ cảnh nơi ngoại lệ xảy ra, dưới dạng truy vết ngăn xếp (stack traceback). Nó cho thấy các dòng mã nguồn liên quan, tuy nhiên nó không hiển thị các dòng nhập từ bàn phím.
Danh sách các ngoại lệ tích hợp có thể được tìm thấy trong Built-in Exceptions (Ngoại lệ Tích hợp).
8.3. Xử Lý Ngoại Lệ (Handling Exceptions)
Có thể viết các chương trình xử lý các ngoại lệ cụ thể. Hãy xem ví dụ sau, chương trình yêu cầu người dùng nhập một số nguyên hợp lệ và tiếp tục yêu cầu nếu đầu vào không hợp lệ. Người dùng có thể gián đoạn chương trình bằng tổ hợp phím Control-C (hoặc phím tắt khác tùy hệ điều hành). Việc gián đoạn này kích hoạt ngoại lệ KeyboardInterrupt
.
while True:
try:
x = int(input("Vui lòng nhập một số: "))
break
except ValueError:
print("Lỗi! Đó không phải là một số hợp lệ. Hãy thử lại...")
Cách hoạt động của lệnh try
- Khối
try
(các lệnh giữatry
vàexcept
) sẽ được thực thi trước. - Nếu không có ngoại lệ, khối
except
bị bỏ qua và chương trình tiếp tục sau khốitry
. - Nếu có ngoại lệ xảy ra trong khối
try
, phần còn lại củatry
bị bỏ qua:
- Nếu ngoại lệ khớp với loại ngoại lệ trong
except
, khốiexcept
sẽ được thực thi. - Nếu không có khối
except
phù hợp, ngoại lệ được truyền ra ngoài. - Nếu không có xử lý nào cho ngoại lệ, chương trình sẽ dừng và hiển thị thông báo lỗi.
Sử dụng nhiều khối except
Có thể có nhiều khối except
để xử lý các loại ngoại lệ khác nhau. Mỗi ngoại lệ sẽ chỉ kích hoạt một khối except
phù hợp nhất.
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print("Lỗi hệ thống:", err)
except ValueError:
print("Không thể chuyển đổi dữ liệu thành số nguyên.")
except Exception as err:
print(f"Lỗi không mong đợi: {err=}, {type(err)=}")
raise
Bắt nhiều loại ngoại lệ cùng lúc
Một khối except
có thể xử lý nhiều loại ngoại lệ bằng cách sử dụng tuple:
except (RuntimeError, TypeError, NameError):
pass
Phân cấp ngoại lệ
Các ngoại lệ trong Python có thể kế thừa lẫn nhau. Khi bắt ngoại lệ, một ngoại lệ con sẽ được xử lý trước ngoại lệ cha của nó.
Ví 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")
Kết quả:
B
C
D
Nếu đảo ngược thứ tự except
(đặt except B
lên đầu), kết quả sẽ là:
B
B
B
Lý do là khi except B
bắt được ngoại lệ, các khối except
phía sau sẽ không được thực thi.
Đối số của ngoại lệ
Ngoại lệ có thể chứa đối số đi kèm, được lưu trong thuộc tính .args
của đối tượng ngoại lệ.
try:
raise Exception('spam', 'eggs')
except Exception as inst:
print(type(inst)) # Loại ngoại lệ
print(inst.args) # Danh sách đối số
print(inst) # Hiển thị ngoại lệ
x, y = inst.args # Giải nén đối số
print('x =', x)
print('y =', y)
Kết quả:
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
Hàm __str__()
của ngoại lệ tự động hiển thị danh sách đối số, giúp bạn không cần truy cập .args
trực tiếp.
8.4. BaseException
và Xử Lý Ngoại Lệ (BaseException and Exception Handling)
BaseException
là lớp cơ sở chung của tất cả các ngoại lệ trong Python. Một trong các lớp con của nó, Exception
, là lớp cơ sở cho tất cả các ngoại lệ không gây dừng chương trình. Các ngoại lệ không phải là lớp con của Exception
thường không được xử lý, vì chúng được sử dụng để chỉ ra rằng chương trình nên kết thúc ngay lập tức.
Ví dụ về các ngoại lệ như vậy bao gồm:
SystemExit
: Được phát sinh bởisys.exit()
.KeyboardInterrupt
: Được phát sinh khi người dùng muốn dừng chương trình (ví dụ, nhấnCtrl+C
).
Sử dụng Exception
để bắt tất cả ngoại lệ
Exception
có thể được sử dụng như một ký hiệu đại diện (wildcard) để bắt hầu hết các ngoại lệ. Tuy nhiên, tốt nhất nên cụ thể hóa loại ngoại lệ cần xử lý để tránh bắt các ngoại lệ không mong muốn và cho phép các ngoại lệ khác tiếp tục lan truyền.
Mẫu phổ biến nhất để xử lý ngoại lệ Exception
là ghi log hoặc in lỗi, sau đó phát sinh lại để cho phép hàm gọi tiếp tục xử lý ngoại lệ nếu cần:
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print("Lỗi hệ thống:", err)
except ValueError:
print("Không thể chuyển đổi dữ liệu thành số nguyên.")
except Exception as err:
print(f"Lỗi không mong đợi: {err=}, {type(err)=}")
raise
8.5. Câu Lệnh try
Với else
Câu lệnh try … except
có thể có một khối else
tùy chọn, phải được đặt sau tất cả các khối except
. Phần else
sẽ chỉ chạy nếu không có ngoại lệ xảy ra trong khối try
. Điều này rất hữu ích cho mã cần được thực thi chỉ khi khối try
thành công.
Ví dụ:
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except OSError:
print('Không thể mở', arg)
else:
print(arg, 'có', len(f.readlines()), 'dòng')
f.close()
Tại sao nên sử dụng else
?
Đặt mã xử lý trong khối else
tốt hơn là đặt vào try
, vì nó tránh bắt nhầm ngoại lệ không đến từ mã chính cần bảo vệ.
8.6. Ngoại Lệ Phát Sinh Trong Hàm Được Gọi (Exceptions Propagating Through Functions)
Trình xử lý ngoại lệ không chỉ xử lý ngoại lệ xảy ra trực tiếp trong khối try
, mà còn có thể xử lý ngoại lệ phát sinh từ các hàm được gọi trong khối try
, kể cả các hàm gọi gián tiếp.
Ví dụ:
def this_fails():
x = 1/0 # Lỗi chia cho 0
try:
this_fails()
except ZeroDivisionError as err:
print('Xử lý lỗi runtime:', err)
Kết quả:
Xử lý lỗi runtime: division by zero
Trong ví dụ trên, mặc dù ngoại lệ ZeroDivisionError
xảy ra trong hàm this_fails()
, nhưng nó vẫn có thể bị bắt bởi khối except
bên ngoài try
.
8.4. Đưa Ra Ngoại Lệ (Raising Exceptions)
Câu lệnh raise
cho phép lập trình viên chủ động phát sinh một ngoại lệ cụ thể. Ví dụ:
>>> raise NameError('HiThere')
Kết quả:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
raise NameError('HiThere')
NameError: HiThere
Đối số duy nhất của raise
chỉ định loại ngoại lệ cần phát sinh. Đối số này phải là một thể hiện ngoại lệ hoặc một lớp ngoại lệ (lớp kế thừa từ BaseException
, chẳng hạn như Exception
hoặc một trong các lớp con của nó). Nếu một lớp ngoại lệ được truyền vào, nó sẽ tự động được khởi tạo mà không cần đối số:
raise ValueError # tương đương với 'raise ValueError()'
Nếu bạn cần kiểm tra xem một ngoại lệ có xảy ra hay không nhưng không có ý định xử lý nó, có thể sử dụng raise
trong khối except
để ném lại ngoại lệ:
>>> try:
... raise NameError('HiThere')
... except NameError:
... print('Một ngoại lệ vừa xảy ra!')
... raise
Kết quả:
Một ngoại lệ vừa xảy ra!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise NameError('HiThere')
NameError: HiThere
8.5. Chuỗi Ngoại Lệ (Exception Chaining)
Nếu một ngoại lệ chưa được xử lý xảy ra bên trong một khối except
, ngoại lệ này sẽ được liên kết với ngoại lệ ban đầu và được bao gồm trong thông báo lỗi:
>>> try:
... open("database.sqlite")
... except OSError:
... raise RuntimeError("Không thể xử lý lỗi")
...
Kết quả:
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("Không thể xử lý lỗi")
RuntimeError: Không thể xử lý lỗi
Để chỉ ra rằng một ngoại lệ là hệ quả trực tiếp của một ngoại lệ khác, câu lệnh raise
cho phép một tùy chọn from
:
# exc phải là một thể hiện ngoại lệ hoặc None.
raise RuntimeError from exc
Điều này hữu ích khi bạn cần biến đổi ngoại lệ. Ví dụ:
>>> def func():
... raise ConnectionError
...
>>> try:
... func()
... except ConnectionError as exc:
... raise RuntimeError('Không thể mở cơ sở dữ liệu') from exc
...
Kết quả:
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('Không thể mở cơ sở dữ liệu') from exc
RuntimeError: Không thể mở cơ sở dữ liệu
Ngoài ra, bạn có thể vô hiệu hóa tự động liên kết ngoại lệ bằng cách sử dụng from None
:
>>> try:
... open('database.sqlite')
... except OSError:
... raise RuntimeError from None
...
Kết quả:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError from None
RuntimeError
Để biết thêm thông tin về cơ chế liên kết ngoại lệ, hãy xem Ngoại lệ Tích hợp (Built-in Exceptions).
8.6. Ngoại Lệ Do Người Dùng Định Nghĩa (User-defined Exceptions)
Các chương trình có thể đặt tên cho các ngoại lệ riêng bằng cách tạo một lớp ngoại lệ mới (xem phần Lớp để biết thêm về lớp trong Python). Các ngoại lệ tùy chỉnh thường nên kế thừa từ lớp Exception
, trực tiếp hoặc gián tiếp.
Lớp ngoại lệ có thể được định nghĩa với đầy đủ các khả năng như bất kỳ lớp nào khác trong Python, nhưng thông thường chúng được giữ đơn giản, chủ yếu chỉ cung cấp một số thuộc tính để truyền tải thông tin về lỗi tới trình xử lý ngoại lệ.
Hầu hết các ngoại lệ đều có tên kết thúc bằng từ “Error”, tuân theo cách đặt tên của các ngoại lệ tiêu chuẩn trong Python.
Nhiều mô-đun tiêu chuẩn định nghĩa ngoại lệ riêng của chúng để báo cáo lỗi có thể xảy ra trong các hàm mà chúng cung cấp.
8.7. Định Nghĩa Hành Động Dọn Dẹp (Defining Clean-up Actions)
Câu lệnh try
có một mệnh đề tùy chọn khác được sử dụng để xác định các hành động dọn dẹp (clean-up actions) cần được thực thi trong mọi trường hợp. Ví dụ:
>>> try:
... raise KeyboardInterrupt
... finally:
... print('Tạm biệt, thế giới!')
...
Tạm biệt, thế giới!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise KeyboardInterrupt
KeyboardInterrupt
Nếu có một mệnh đề finally
, thì nó sẽ được thực thi như một tác vụ cuối cùng trước khi câu lệnh try
hoàn thành. Khối finally
luôn được thực thi bất kể câu lệnh try
có phát sinh ngoại lệ hay không. Các điểm sau đây mô tả các trường hợp phức tạp hơn khi có ngoại lệ xảy ra:
- Nếu có ngoại lệ trong khối
try
, ngoại lệ đó có thể được xử lý trong khốiexcept
. Nếu không có khốiexcept
nào xử lý ngoại lệ, nó sẽ được phát sinh lại sau khi khốifinally
đã thực thi. - Nếu có ngoại lệ xảy ra trong khối
except
hoặcelse
, nó cũng sẽ được phát sinh lại sau khi khốifinally
đã thực thi. - Nếu khối
finally
chứa câu lệnhbreak
,continue
hoặcreturn
, ngoại lệ sẽ không được phát sinh lại. - Nếu khối
try
đạt đến câu lệnhbreak
,continue
hoặcreturn
, khốifinally
sẽ thực thi trước khi câu lệnh đó được thực hiện. - Nếu khối
finally
chứa một câu lệnhreturn
, giá trị trả về sẽ là giá trị từ câu lệnhreturn
trongfinally
, không phải giá trị từtry
.
Ví dụ:
>>> def bool_return():
... try:
... return True
... finally:
... return False
...
>>> bool_return()
False
Ví dụ phức tạp hơn:
>>> def divide(x, y):
... try:
... result = x / y
... except ZeroDivisionError:
... print("Lỗi chia cho 0!")
... else:
... print("Kết quả là", result)
... finally:
... print("Thực thi khối finally")
...
>>> divide(2, 1)
Kết quả là 2.0
Thực thi khối finally
>>> divide(2, 0)
Lỗi chia cho 0!
Thực thi khối finally
>>> divide("2", "1")
Thực thi khối finally
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'
Như bạn có thể thấy, khối finally
luôn được thực thi. Ngoại lệ TypeError
do phép chia hai chuỗi gây ra không được xử lý trong khối except
, vì vậy nó được phát sinh lại sau khi khối finally
hoàn thành.
Trong các ứng dụng thực tế, khối finally
rất hữu ích để giải phóng tài nguyên bên ngoài (chẳng hạn như tệp hoặc kết nối mạng), bất kể việc sử dụng tài nguyên đó có thành công hay không.
8.8. Các Hành Động Dọn Dẹp Được Xác Định Trước (Predefined Clean-up Actions)
Một số đối tượng trong Python định nghĩa các hành động dọn dẹp tiêu chuẩn sẽ được thực hiện khi đối tượng không còn cần thiết nữa, bất kể thao tác sử dụng đối tượng đó có thành công hay thất bại.
Hãy xem ví dụ sau, trong đó một tệp được mở và nội dung của nó được in ra màn hình:
for line in open("myfile.txt"):
print(line, end="")
Vấn đề của đoạn mã này là nó để lại tệp mở trong một khoảng thời gian không xác định sau khi đoạn mã đã thực thi xong. Điều này không phải là vấn đề trong các tập lệnh đơn giản, nhưng có thể gây ra vấn đề trong các ứng dụng lớn hơn.
Câu lệnh with
cho phép các đối tượng như tệp được sử dụng theo cách đảm bảo rằng chúng luôn được dọn dẹp đúng cách và kịp thời:
with open("myfile.txt") as f:
for line in f:
print(line, end="")
Sau khi câu lệnh with
hoàn thành, tệp f
luôn được đóng, ngay cả khi có lỗi xảy ra trong quá trình xử lý nội dung tệp.
Các đối tượng có hành động dọn dẹp được xác định trước (như tệp) sẽ đề cập đến điều này trong tài liệu hướng dẫn của chúng.
8.9. Phát Sinh và Xử Lý Nhiều Ngoại Lệ Không Liên Quan (Raising and Handling Multiple Unrelated Exceptions)
Có những trường hợp cần báo cáo nhiều ngoại lệ xảy ra cùng lúc. Điều này thường gặp trong các hệ thống xử lý đồng thời (concurrency frameworks), nơi nhiều tác vụ có thể thất bại song song. Ngoài ra, cũng có những tình huống khác trong đó tiếp tục thực thi và thu thập nhiều lỗi sẽ hữu ích hơn là chỉ phát sinh ngoại lệ đầu tiên.
Lớp ngoại lệ tích hợp ExceptionGroup
cho phép gói nhiều ngoại lệ thành một nhóm và phát sinh chúng cùng nhau. Nó cũng là một ngoại lệ, vì vậy có thể được bắt giống như bất kỳ ngoại lệ nào khác.
Ví dụ:
>>> def f():
... excs = [OSError('error 1'), SystemError('error 2')]
... raise ExceptionGroup('Có lỗi xảy ra', excs)
...
>>> f()
Kết quả:
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| f()
| ~^^
| File "<stdin>", line 3, in f
| raise ExceptionGroup('Có lỗi xảy ra', excs)
| ExceptionGroup: Có lỗi xảy ra (2 ngoại lệ con)
+-+---------------- 1 ----------------
| OSError: error 1
+---------------- 2 ----------------
| SystemError: error 2
+------------------------------------
Khi bắt nhóm ngoại lệ, chúng ta có thể xử lý chúng bằng except
thông thường:
>>> try:
... f()
... except Exception as e:
... print(f'Đã bắt {type(e)}: {e}')
...
Kết quả:
Đã bắt <class 'ExceptionGroup'>: Có lỗi xảy ra (2 ngoại lệ con)
Xử lý có chọn lọc các ngoại lệ trong nhóm
Sử dụng except*
thay vì except
cho phép chúng ta chỉ xử lý các ngoại lệ trong nhóm phù hợp với một kiểu cụ thể. Trong ví dụ sau, mỗi khối except*
sẽ trích xuất và xử lý riêng các ngoại lệ của một kiểu cụ thể, trong khi các ngoại lệ khác tiếp tục được lan truyền và có thể bị bắt bởi khối except*
khác hoặc bị phát sinh lại.
Ví dụ:
>>> def f():
... raise ExceptionGroup(
... "group1",
... [
... OSError(1),
... SystemError(2),
... ExceptionGroup(
... "group2",
... [
... OSError(3),
... RecursionError(4)
... ]
... )
... ]
... )
...
>>> try:
... f()
... except* OSError as e:
... print("Có lỗi OSError")
... except* SystemError as e:
... print("Có lỗi SystemError")
...
Kết quả:
Có lỗi OSError
Có lỗi SystemError
+ 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 ngoại lệ con)
+-+---------------- 1 ----------------
| ExceptionGroup: group2 (1 ngoại lệ con)
+-+---------------- 1 ----------------
| RecursionError: 4
+------------------------------------
Lưu ý rằng các ngoại lệ nằm trong nhóm phải là thể hiện ngoại lệ, không phải là kiểu ngoại lệ. Điều này là do trên thực tế, các ngoại lệ thường là những lỗi đã được phát sinh và bắt lại bởi chương trình. Mẫu sau đây minh họa một trường hợp phổ biến:
>>> excs = []
... for test in tests:
... try:
... test.run()
... except Exception as e:
... excs.append(e)
...
>>> if excs:
... raise ExceptionGroup("Lỗi kiểm thử", excs)
...
8.10. Bổ Sung Thông Tin vào Ngoại Lệ (Enriching Exceptions with Notes)
Khi một ngoại lệ được tạo ra để phát sinh, nó thường được khởi tạo với thông tin mô tả lỗi đã xảy ra. Tuy nhiên, trong một số trường hợp, có thể cần bổ sung thông tin sau khi ngoại lệ đã được bắt.
Để làm điều này, các ngoại lệ có phương thức add_note(note)
, nhận một chuỗi và thêm nó vào danh sách ghi chú của ngoại lệ. Khi traceback được hiển thị, tất cả các ghi chú sẽ xuất hiện theo thứ tự chúng được thêm vào.
Ví dụ:
>>> try:
... raise TypeError('Kiểu dữ liệu không hợp lệ')
... except Exception as e:
... e.add_note('Thêm một số thông tin bổ sung')
... e.add_note('Thêm một số thông tin khác')
... raise
...
Kết quả:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise TypeError('Kiểu dữ liệu không hợp lệ')
TypeError: Kiểu dữ liệu không hợp lệ
Thêm một số thông tin bổ sung
Thêm một số thông tin khác
Ứng dụng trong Nhóm Ngoại Lệ (Exception Groups)
Khi thu thập các ngoại lệ vào một nhóm ngoại lệ (exception group), có thể muốn thêm thông tin ngữ cảnh cho từng lỗi riêng lẻ. Trong ví dụ sau, mỗi ngoại lệ trong nhóm có một ghi chú cho biết lỗi đã xảy ra trong vòng lặp nào.
>>> def f():
... raise OSError('Thao tác thất bại')
...
>>> excs = []
>>> for i in range(3):
... try:
... f()
... except Exception as e:
... e.add_note(f'Xảy ra trong lần lặp {i+1}')
... excs.append(e)
...
>>> raise ExceptionGroup('Có một số vấn đề', excs)
Kết quả:
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| raise ExceptionGroup('Có một số vấn đề', excs)
| ExceptionGroup: Có một số vấn đề (3 ngoại lệ con)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('Thao tác thất bại')
| OSError: Thao tác thất bại
| Xảy ra trong lần lặp 1
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('Thao tác thất bại')
| OSError: Thao tác thất bại
| Xảy ra trong lần lặp 2
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('Thao tác thất bại')
| OSError: Thao tác thất bại
| Xảy ra trong lần lặp 3
+------------------------------------