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ỗiexcept— xử lý loại lỗi cụ thểelse— chạy khi không có lỗi nào xảy rafinally— 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.