Lỗi chỉ là những giá trị khó chịu
Khi có gì đó trục trặc trong Python, chẳng hạn chia cho 0, đọc một tệp không tồn tại hay chuyển đổi một con số sai định dạng, runtime tạo ra một đối tượng ngoại lệ (exception) rồi lần ngược call stack cho đến khi có chỗ bắt được nó. Nếu không có chỗ nào bắt, chương trình dừng lại và in ra traceback.
Bản thân ngoại lệ không phải điều xấu. Đó là cách Python báo hiệu "Tôi không thể tiếp tục thao tác này, và đây là lý do." Việc của bạn là quyết định, theo từng trường hợp, ngoại lệ nào bạn biết cách khắc phục và ngoại lệ nào nên để lan lên trên.
Cấu trúc cơ bản
try:mở khối code có thể gây lỗi.except ValueError:bắt đúng ngoại lệ đó nếu nó xảy ra bên trongtry.- Nếu không có ngoại lệ nào, khối
exceptbị bỏ qua hoàn toàn.
Chạy đoạn code với 42: chạy bình thường. Chạy với hello: khối xử lý lỗi sẽ chạy.
Bắt đúng loại ngoại lệ
Python có một hệ thống phân cấp các kiểu ngoại lệ. Vài loại bạn sẽ gặp thường xuyên:
ValueError: một giá trị bị sai theo cách nào đó (int("abc"), đối số nằm ngoài khoảng cho phép).TypeError: dùng sai kiểu dữ liệu ("hi" + 3).KeyError: không tìm thấy khóa trong dict.IndexError: chỉ số của một dãy nằm ngoài phạm vi.FileNotFoundError: tệp không tồn tại.ZeroDivisionError: cố chia cho 0.AttributeError: đối tượng không có thuộc tính được yêu cầu.
Hãy bắt đúng loại mà bạn biết cách xử lý:
Bạn có thể bắt nhiều ngoại lệ trong cùng một mệnh đề bằng cách truyền vào một tuple:
Để ý as e. Cú pháp này gán đối tượng ngoại lệ vào e để bạn xem thông báo lỗi hoặc các thuộc tính của nó.
Đừng bắt mọi thứ
Một except: trống bắt đúng nghĩa là mọi thứ, kể cả KeyboardInterrupt (khi bạn nhấn Ctrl-C) và các lệnh thoát ở cấp hệ thống. Đừng dùng nó.
except Exception: khá hơn một chút nhưng vẫn nguy hiểm: nó nuốt mất những bug bạn không lường trước và che giấu nguồn gốc thật của vấn đề:
# Don't do this without a really good reason.
try:
do_something()
except Exception:
pass
Cách đúng gần như luôn là bắt đúng ngoại lệ mà bạn biết cách khắc phục. Nếu một ngoại lệ ngoài dự kiến lên tới tầng trên cùng của chương trình, traceback sẽ cho bạn biết chính xác chuyện gì đã xảy ra. Đó là tính năng, không phải bug.
else và finally
Câu lệnh try còn có hai mệnh đề không bắt buộc nữa:
elsechạy khi khốitrykết thúc mà không ném ra ngoại lệ nào.finallyluôn chạy, dù có ngoại lệ hay không.
else là chỗ gọn gàng nhất cho code "chỉ chạy khi thành công": bạn không muốn khối try làm nhiều hơn phần thật sự có thể lỗi. finally dành cho việc dọn dẹp bắt buộc phải chạy kể cả khi try thất bại: đóng một tài nguyên, nhả một khóa (lock), khôi phục trạng thái.
Tự raise ngoại lệ
Dùng raise để báo lỗi trong code của chính bạn:
Chọn kiểu ngoại lệ phù hợp với lỗi đã xảy ra. Ưu tiên các kiểu có sẵn trước, như ValueError, TypeError, FileNotFoundError, rồi mới tự định nghĩa lớp riêng.
Tự định nghĩa ngoại lệ
Khi các kiểu có sẵn không diễn tả đúng ý nghĩa, hãy định nghĩa một ngoại lệ tùy chỉnh:
Kế thừa từ Exception (hoặc một kiểu có sẵn cụ thể hơn) và viết docstring cho lớp đó. Thường thì chỉ cần vậy. Ngoại lệ tùy chỉnh giúp nơi gọi hàm bắt đúng loại lỗi có ý nghĩa với nghiệp vụ của họ.
raise ... from ...: ngoại lệ nối chuỗi
Khi một ngoại lệ gây ra ngoại lệ khác, hãy giữ lại chuỗi liên kết:
from e gắn kèm lỗi gốc. Khi in traceback, Python hiển thị cả hai: ConfigError được báo ra và FileNotFoundError đã gây ra nó. Dấu vết như vậy cực kỳ có giá trị khi gỡ lỗi.
Context manager: cách dọn dẹp gọn hơn
finally vẫn ổn, nhưng với các tài nguyên như tệp, một context manager (thứ mà with dùng) gần như luôn tốt hơn:
# finally version
f = open("data.txt")
try:
data = f.read()
finally:
f.close()
# with version
with open("data.txt") as f:
data = f.read()
Cả hai đều an toàn. Dạng with ngắn hơn và tự động áp dụng. Chỉ dùng finally khi bạn làm việc gì đó mà thư viện chuẩn chưa bọc sẵn trong một context manager.
Khi nào không nên bắt
Bắt một ngoại lệ là một quyết định: bạn đang nói "Tôi xử lý được chuyện này." Nếu không xử lý được, hãy để ngoại lệ lan lên. Code như thế này gần như luôn là sai lầm:
try:
do_work()
except Exception:
pass # silently ignore everything
Làm im lỗi khiến bug trở nên vô hình. Thà chương trình dừng hẳn với thông báo rõ ràng còn hơn chạy khập khiễng trong một trạng thái không nhất quán.
Tổng kết
try/exceptgiúp bạn xử lý những lỗi có thể khắc phục.- Bắt đúng loại ngoại lệ, đừng bắt
Exceptionmột cách chung chung. raisebáo lỗi trong code của chính bạn.- Khối
withthay thế phần lớn việc dọn dẹp bằngfinally. - Khi còn phân vân, cứ để ngoại lệ lan lên.
Tiếp theo: điểm qua những lỗi Python hay ném ra nhất, như KeyError, ValueError, ModuleNotFoundError và vài lỗi khác, cùng các thói quen gỡ lỗi giúp sửa chúng nhanh chóng.
Câu hỏi thường gặp
Làm sao để xử lý lỗi trong Python?
Đặt đoạn code có thể gây lỗi vào khối try và bắt đúng loại ngoại lệ trong khối except: try: risky() except ValueError: .... Khối else (không bắt buộc) chạy khi không có ngoại lệ nào; finally luôn chạy dù có lỗi hay không, dùng để dọn dẹp.
Có nên bắt Exception cho an toàn không?
Không. Một except: trống hoặc except Exception: sẽ che mất những bug bạn không lường trước. Chỉ bắt đúng ngoại lệ mà bạn biết cách khắc phục, phần còn lại cứ để lan lên để bạn thấy vấn đề thật sự.
raise và raise from khác nhau thế nào?
raise NewError(...) ném ra một ngoại lệ mới. raise NewError(...) from original giữ ngoại lệ gốc làm nguyên nhân, và Python hiển thị nó trong traceback. Dùng from khi một lỗi ở tầng thấp gây ra lỗi ở tầng cao hơn mà bạn muốn báo ra ngoài.