Hàm và xử lý ngoại lệ trong Python

Hàm giúp bạn tách chương trình thành những phần nhỏ có thể tái sử dụng. Xử lý ngoại lệ giúp chương trình không sập khi gặp tình huống bất thường. Hai chủ đề này đi cùng nhau vì cả hai đều nói về việc viết code đáng tin cậy.

Định nghĩa hàm

def chao(ten, loi_chao="Xin chào"):
    return f"{loi_chao}, {ten}!"

print(chao("Lan"))                       # Xin chào, Lan!
print(chao("Nam", "Chào buổi sáng"))     # Chào buổi sáng, Nam!

Tham số có giá trị mặc định phải đặt sau các tham số bắt buộc. Bạn cũng có thể gọi hàm bằng tên tham số, giúp code dễ đọc hơn nhiều khi hàm có nhiều đối số:

print(chao(ten="Hoa", loi_chao="Chào bạn"))

Số lượng đối số linh hoạt

def tinh_tong(*so):
    return sum(so)

print(tinh_tong(1, 2, 3))        # 6
print(tinh_tong(1, 2, 3, 4, 5))  # 15

def tao_ho_so(**thong_tin):
    for khoa, gia_tri in thong_tin.items():
        print(f"{khoa}: {gia_tri}")

tao_ho_so(ten="Minh", tuoi=30, thanh_pho="Hà Nội")

*args gom các đối số vị trí thành tuple, **kwargs gom các đối số có tên thành dict.

Ép dùng tham số có tên

Khi hàm có nhiều tham số boolean, việc gọi xu_ly(data, True, False) khiến người đọc không hiểu gì. Dấu * buộc các tham số sau nó phải gọi bằng tên:

def xu_ly(du_lieu, *, sap_xep=False, loai_trung=False):
    ...

xu_ly(ds, sap_xep=True)          # rõ ràng
xu_ly(ds, True)                  # TypeError — bị chặn

Phạm vi biến

Biến tạo bên trong hàm chỉ tồn tại trong hàm đó:

dem = 0

def tang():
    dem = dem + 1     # UnboundLocalError

def tang_dung():
    global dem
    dem += 1

Tuy nhiên, dùng global thường là dấu hiệu thiết kế chưa tốt. Cách sạch hơn là trả về giá trị mới:

def tang(gia_tri):
    return gia_tri + 1

dem = tang(dem)

Xử lý ngoại lệ

Khi chương trình gặp lỗi, Python ném ra một ngoại lệ. Nếu không xử lý, chương trình dừng lại:

try:
    so = int(input("Nhập một số: "))
    ket_qua = 100 / so
except ValueError:
    print("Bạn phải nhập số")
except ZeroDivisionError:
    print("Không chia được cho 0")
else:
    print(f"Kết quả: {ket_qua}")
finally:
    print("Đã xử lý xong")

Bốn khối này có vai trò khác nhau:

  • try — đoạn code có thể gây lỗi
  • except — xử lý loại lỗi cụ thể
  • else — chạy khi không có lỗi nào xảy ra
  • finally — luôn chạy, dù có lỗi hay không

Đừng bắt tất cả ngoại lệ

Đây là cách viết cần tránh:

try:
    xu_ly_du_lieu()
except:                  # bắt mọi thứ, kể cả Ctrl+C
    pass                 # rồi im lặng nuốt lỗi

Kiểu code này che giấu lỗi thật, khiến việc gỡ rối sau này trở nên rất khó. Hãy bắt đúng loại ngoại lệ bạn dự đoán được, và nếu buộc phải bắt rộng, ít nhất hãy ghi log lại:

import logging

try:
    xu_ly_du_lieu()
except Exception as e:
    logging.exception("Lỗi khi xử lý dữ liệu")
    raise                # ném lại để tầng trên biết

Tự định nghĩa ngoại lệ

Với ứng dụng có nghiệp vụ riêng, việc tạo ngoại lệ riêng giúp code diễn đạt đúng ý:

class SoDuKhongDu(Exception):
    """Ném ra khi tài khoản không đủ số dư."""

def rut_tien(so_du, so_tien):
    if so_tien > so_du:
        raise SoDuKhongDu(f"Cần {so_tien}, chỉ có {so_du}")
    return so_du - so_tien

try:
    rut_tien(100_000, 500_000)
except SoDuKhongDu as e:
    print(f"Giao dịch thất bại: {e}")

Dùng context manager thay cho finally

Khi làm việc với file hoặc kết nối mạng, câu lệnh with đảm bảo tài nguyên luôn được giải phóng:

with open("du_lieu.txt", encoding="utf-8") as f:
    noi_dung = f.read()
# file tự động đóng, kể cả khi có lỗi xảy ra bên trong

Cách này ngắn gọn và an toàn hơn việc tự viết try/finally để gọi f.close().

Docstring

Hàm nên có mô tả ngắn về việc nó làm:

def tinh_lai_kep(von, lai_suat, nam):
    """Tính số tiền sau khi gửi tiết kiệm lãi kép.

    Args:
        von: Số tiền gửi ban đầu.
        lai_suat: Lãi suất năm, dạng thập phân (0.07 tức 7%).
        nam: Số năm gửi.

    Returns:
        Tổng số tiền nhận được sau kỳ hạn.
    """
    return von * (1 + lai_suat) ** nam

Docstring không chỉ để người khác đọc — bạn có thể xem nó ngay trong trình thông dịch bằng help(tinh_lai_kep), và hầu hết các trình soạn thảo đều hiển thị nó khi bạn gõ tên hàm.