Lỗi và Ngoại Lệ (Errors and Exceptions)

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

  1. Khối try (các lệnh giữa tryexcept) sẽ được thực thi trước.
  2. 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ối try.
  3. Nếu có ngoại lệ xảy ra trong khối try, phần còn lại của try bị bỏ qua:
  • Nếu ngoại lệ khớp với loại ngoại lệ trong except, khối except 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ởi sys.exit().
  • KeyboardInterrupt: Được phát sinh khi người dùng muốn dừng chương trình (ví dụ, nhấn Ctrl+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ệ Exceptionghi 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ối except. Nếu không có khối except nào xử lý ngoại lệ, nó sẽ được phát sinh lại sau khi khối finally đã thực thi.
  • Nếu có ngoại lệ xảy ra trong khối except hoặc else, nó cũng sẽ được phát sinh lại sau khi khối finally đã thực thi.
  • Nếu khối finally chứa câu lệnh break, continue hoặc return, ngoại lệ sẽ không được phát sinh lại.
  • Nếu khối try đạt đến câu lệnh break, continue hoặc return, khối finally 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ệnh return, giá trị trả về sẽ là giá trị từ câu lệnh return trong finally, 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
    +------------------------------------

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top