2.26. Ném ngoại lệ

Một hàm có thể báo hiệu vấn đề cho bên gọi nó bằng cách ném một ngoại lệ. Từ khóa là raise:

def square_root(x):
    if x < 0:
        raise ValueError("square_root expects a non-negative number")
    return x ** 0.5

Gọi square_root(-1) dừng lại ở dòng raise, thoát ra khỏi hàm, và tìm kiếm except phù hợp ở bên gọi. Nếu không có bên gọi nào bắt nó, tập lệnh kết thúc với một traceback.

2.26.1. Tại sao ném ngoại lệ thay vì trả về giá trị sentinel

Hai cách để báo cáo "đầu vào không hợp lệ":

# signal with a sentinel
def square_root_or_none(x):
    if x < 0:
        return None
    return x ** 0.5

# raise an exception
def square_root(x):
    if x < 0:
        raise ValueError("...")
    return x ** 0.5

Dạng ngoại lệ thường tốt hơn:

  • Bên gọi phải cố ý xử lý trường hợp lỗi -- hoặc bằng try, hoặc bằng cách để ngoại lệ truyền tiếp. Giá trị sentinel dễ bị quên và dễ nhầm với kết quả bình thường.

  • Thông báo lỗi đi kèm với ngoại lệ; cách tiếp cận sentinel phải đính kèm thông tin chẩn đoán ở nơi khác.

  • Hành vi mặc định khi ngoại lệ không được xử lý là một lỗi sập ồn ào với traceback trỏ vào lời gọi vi phạm. Các giá trị trả về None thầm lặng trở thành lỗi khó phát hiện về sau.

Hãy dùng sentinel chỉ khi "không tìm thấy" là kết quả bình thường, không phải ngoại lệ -- dict.get() trả về None khi thiếu khóa chính xác vì các phép tra cứu đôi khi không tìm thấy là điều được mong đợi.

2.26.2. Các lớp ngoại lệ tùy chỉnh

Để ném một vấn đề mà bên gọi có thể muốn phân biệt với các lỗi tích hợp, hãy định nghĩa một lớp con của Exception:

class ConfigError(Exception):
    pass

def load_config(path):
    try:
        f = open(path)
    except OSError as e:
        raise ConfigError("missing config file: " + path)

try:
    load_config("settings.json")
except ConfigError as e:
    print("startup failed:", e)

Thân class rỗng là ổn -- chính cái tên mới là điều quan trọng, vì bên gọi bắt theo lớp. Nhóm các lỗi liên quan dưới một lớp cơ sở chung nếu bên gọi có thể muốn bắt toàn bộ nhóm trong một khối.

2.26.2.1. Ném lại ngoại lệ

Một raise trần bên trong khối except ném lại ngoại lệ hiện tại để nó truyền đến trình xử lý tiếp theo:

try:
    do_work()
except Exception as e:
    log(e)
    raise        # let it keep going

Đây là dạng đúng khi một hàm muốn quan sát một lỗi (ghi log, đếm, hoàn tác thay đổi một phần) mà không thực sự xử lý nó.

2.26.3. Khi nào nên bắt và khi nào nên truyền tiếp

Một quy tắc ngón tay hữu ích:

  • Bắt ngoại lệ ở mức có thể phục hồi có ý nghĩa -- thay thế bằng giá trị mặc định, thử lại, bỏ qua đầu vào không hợp lệ.

  • Để nó truyền tiếp khi không có gì hữu ích để làm ngoài việc sập, hoặc khi lớp trên mới là lớp biết cách phục hồi.

Một hàm ở giữa ngăn xếp lời gọi nuốt mọi lỗi và trả về thầm lặng làm cho các lỗi không thể truy tìm. Ưu tiên để ngoại lệ di chuyển cho đến khi chúng đến mã thực sự có kế hoạch xử lý.