Trong bài này
Lan viết chương trình tính tiền cho 4 bạn trong câu lạc bộ. Một lần, bạn nhập chữ thay cho số; lần khác, số người là 0. Lan cần tìm nguyên nhân và xử lý những tình huống có thể dự đoán trước.
Traceback cho biết nơi chương trình gặp lỗi
Chạy đoạn sau để xem Python thông báo điều gì:
diem = [8, 7, 9]
print(diem[3])Lỗi khi chạy, ở dòng 2
IndexError: list index out of range
Danh sách có ba phần tử, mang chỉ số từ 0 đến 2. Chỉ số 3 không tồn tại. Python dừng và hiển thị dấu vết lỗi (traceback). Dòng cuối nêu loại IndexError và lời mô tả. Phần phía trên cho biết dòng code đang chạy khi lỗi xảy ra.
Nếu lỗi xảy ra trong hàm được gọi từ nơi khác, traceback có thể ghi nhiều dòng vị trí. Đọc từ lời gọi ở phía trên xuống dòng gần cuối. Dòng gần cuối thường chỉ nơi lỗi phát sinh; dòng cuối cho biết loại lỗi. Số dòng và đường dẫn thực tế phụ thuộc nơi chạy.
def trung_binh(a):
return sum(a) / len(a)
print(trung_binh([]))Lỗi khi chạy, ở dòng 2
ZeroDivisionError: division by zero
Traceback ghi dòng gọi trung_binh([]) ở phía trên. Gần cuối là dòng return trong hàm, nơi phép chia cho 0 xảy ra. Dòng cuối nêu loại lỗi ZeroDivisionError.
ĐọcXem loại lỗi ở dòng cuốiTìmXác định dòng gây lỗiXemKiểm tra dữ liệu tại dòng đóSửaChạy lại sau khi sửa
Không phải mọi kết quả sai đều tạo traceback. Một chương trình vẫn chạy xong nhưng tính sai thì cần thêm ca thử để phát hiện.
Tên loại lỗi giúp thu hẹp nguyên nhân
Python kiểm tra cú pháp trước khi chạy chương trình. Thiếu dấu hai chấm sau if tạo SyntaxError:
if 5 > 3
print("Dung")Lỗi khi chạy, ở dòng 1
SyntaxError: expected ':'
Sửa dấu câu và chạy lại. Với lỗi cú pháp, vị trí được chỉ có thể nằm gần chỗ sai; cần xem cả dòng trước đó.
Tên biến chưa được gán gây NameError:
so_sach = 5
print(so_sachh)Lỗi khi chạy, ở dòng 2
NameError: name 'so_sachh' is not defined. Did you mean: 'so_sach'?
Kiểm tra tên biến và phạm vi của nó. Python phân biệt chữ hoa với chữ thường.
Phép toán giữa các kiểu không phù hợp có thể gây TypeError:
so_ve = "4"
print(so_ve + 2)Lỗi khi chạy, ở dòng 2
TypeError: can only concatenate str (not "int") to str
Nếu chuỗi thực sự biểu diễn một số nguyên, dùng int(so_ve) trước khi tính. Dùng str(2) chỉ khi bạn muốn nối chữ.
Một giá trị có kiểu phù hợp nhưng nội dung không chuyển được cũng có thể gây lỗi:
so_ve = int("bon")
print(so_ve)Lỗi khi chạy, ở dòng 1
ValueError: invalid literal for int() with base 10: 'bon'
int nhận chuỗi, nhưng "bon" không phải cách viết số nguyên mà hàm chuyển được. Python báo ValueError. Chuỗi "4" lại chuyển được.
Truy cập khóa không có trong dictionary gây KeyError:
gia = {"but": 5, "vo": 8}
print(gia["thuoc"])Lỗi khi chạy, ở dòng 2
KeyError: 'thuoc'
Kiểm tra khóa bằng in hoặc dùng get với giá trị mặc định khi điều đó phù hợp. Giá trị mặc định không tự thêm khóa mới.
Chia cho 0 gây ZeroDivisionError:
tong = 24
so_ban = 0
print(tong / so_ban)Lỗi khi chạy, ở dòng 3
ZeroDivisionError: division by zero
Kiểm tra số chia hoặc xử lý lỗi nếu đầu vào có thể chứa 0. Đừng thay kết quả tùy ý khi đề bài chưa quy định cách xử lý.
Dùng try và except cho lỗi đã dự đoán
Một người nhập số vé trên một dòng. Ta dự đoán họ có thể nhập chữ thay vì số:
bon
chuoi = input()
try:
so_ve = int(chuoi)
except ValueError:
print("Hay nhap so nguyen")
else:
print("So ve:", so_ve)Hay nhap so nguyen
Python thử chạy khối try. Nếu int(chuoi) phát sinh ValueError, khối except ValueError chạy. Phần còn lại của try bị bỏ qua. Nếu không có lỗi, khối else chạy. Trong ví dụ, thông báo yêu cầu nhập lại được in; else không chạy.
Ngoại lệ (exception) là tín hiệu Python phát ra khi một thao tác gặp tình huống bất thường. except ValueError chỉ xử lý ngoại lệ thuộc loại đã nêu. Dấu hai chấm và thụt lề xác định các khối lệnh.
Thử cùng chương trình với đầu vào hợp lệ:
12
chuoi = input()
try:
so_ve = int(chuoi)
except ValueError:
print("Hay nhap so nguyen")
else:
print("So ve:", so_ve)So ve: 12
Ở đây int thành công và else in số vé. Không đặt code cần so_ve sau khối xử lý nếu nhánh lỗi chưa gán biến này.
Một khối try có thể có các nhánh except khác nhau. Mỗi nhánh xử lý một loại lỗi dự đoán được:
0
chuoi = input()
try:
so_ban = int(chuoi)
moi_ban = 24 / so_ban
except ValueError:
print("So ban khong hop le")
except ZeroDivisionError:
print("So ban phai khac 0")
else:
print(moi_ban)So ban phai khac 0
Nếu nhập 0, phép chuyển thành công nhưng phép chia gây ZeroDivisionError. Nhánh tương ứng chạy. Nếu nhập hai, lỗi xuất hiện ngay ở int và phép chia chưa chạy.
Vị trí của thao tác cũng quyết định nhánh nào có thể xử lý lỗi. Các except trong cấu trúc này xử lý lỗi phát sinh bên trong try. Nếu đặt phép chia trong else, except bên trên không bắt lỗi từ phép chia ấy. Hãy đặt cả phép chuyển và phép chia trong try khi cần xử lý hai lỗi tương ứng.
Khi một lệnh trong try gây lỗi, Python không hoàn tất lệnh đó. Ví dụ sau giữ lại kết quả của phép chuyển đầu tiên, nhưng không chạy dòng thông báo sau phép chuyển thứ hai:
try:
so_hop = int("4")
so_ban = int("hai")
print("Da doc ca hai so")
except ValueError:
print("Khong doc duoc so ban")
print("So hop da doc:", so_hop)Khong doc duoc so ban So hop da doc: 4
so_hop được gán trước khi lỗi xảy ra. Tuy nhiên, không nên dựa vào một biến có thể chưa được gán khi dữ liệu khác làm lệnh trước đó thất bại. Với kết quả cần cả hai số, đặt bước dùng chúng trong else.
Giữ khối try đủ nhỏ và chỉ bắt loại lỗi có cách xử lý rõ ràng. Một except không ghi loại lỗi có thể che cả lỗi lập trình khác. Ví dụ sau chỉ bắt ValueError, nên lỗi tên biến vẫn hiện ra:
try:
so = int("4")
print(so_ve)
except ValueError:
print("Khong phai so")Lỗi khi chạy, ở dòng 3
NameError: name 'so_ve' is not defined
NameError không phải ValueError. Nhờ đó bạn thấy tên sai và sửa nguyên nhân thay vì tưởng dữ liệu nhập sai.
finally chạy khi rời khỏi khối try
Đôi khi chương trình cần thực hiện một bước kết thúc dù thao tác trước thành công hay gặp lỗi dự đoán được:
chuoi = "sau"
try:
so = int(chuoi)
except ValueError:
print("Khong doi duoc")
else:
print("Da doi:", so)
finally:
print("Ket thuc luot kiem tra")Khong doi duoc Ket thuc luot kiem tra
finally chạy sau nhánh xử lý lỗi. Khi chuyển đổi thành công, else chạy trước finally. finally còn chạy khi một ngoại lệ chưa được xử lý đi ra khỏi try; sau đó lỗi vẫn tiếp tục được báo.
try:
print(10 / 0)
finally:
print("Da thu phep chia")Lỗi khi chạy, ở dòng 2
ZeroDivisionError: division by zero
Thông báo trong finally được in nhưng ZeroDivisionError vẫn xảy ra. finally không thay thế except và không có nghĩa là thao tác đã thành công.
ThửChạy khối tryChọnLỗi phù hợp: except; không lỗi: elseKết thúcChạy finallyRời khốiTiếp tục hoặc báo lỗi chưa xử lý
In giá trị trung gian để tìm bước tính sai
Đoạn code sau cộng giá vé rồi giảm 2 đơn vị. Kết quả không như mong đợi vì phép trừ nằm trong vòng lặp:
gia = [5, 8, 6]
tong = 0
for gia_ve in gia:
tong += gia_ve
tong -= 2
print("Sau ve", gia_ve, "tong la", tong)
print("Ket qua:", tong)Sau ve 5 tong la 3 Sau ve 8 tong la 9 Sau ve 6 tong la 13 Ket qua: 13
Các dòng theo dõi cho thấy mỗi lượt đều giảm 2. Đó là dò lỗi (debugging): quan sát trạng thái rồi so sánh với điều mong đợi. Bảng sau cho thấy tổng đúng sau từng lần cộng, trước khi giảm:
| Lượt | Giá vé | Tổng |
|---|---|---|
| 1 | 5 | 5 |
| 2 | 8 | 13 |
| 3 | 6 | 19 |
Nếu chỉ giảm một lần cho cả đơn, chuyển phép trừ ra sau vòng lặp:
gia = [5, 8, 6]
tong = 0
for gia_ve in gia:
tong += gia_ve
tong -= 2
print(tong)17
Khi nộp bài cho máy chấm, bỏ các lệnh print dùng để dò lỗi. Đầu ra cần đúng định dạng đề yêu cầu; dòng theo dõi thừa cũng có thể làm bài bị chấm sai.
Tự tạo ca kiểm thử và dùng assert
Một ca kiểm thử (test case) gồm dữ liệu vào và kết quả mong đợi. Hãy tự tính kết quả trước khi chạy hàm. Ví dụ, hàm sau đếm điểm từ 8 trở lên:
def dem_diem_cao(diem):
dem = 0
for x in diem:
if x >= 8:
dem += 1
return dem
print(dem_diem_cao([7, 8, 10]))2
Danh sách có hai điểm đạt ngưỡng. Thử thêm danh sách rỗng, điểm ngay dưới ngưỡng và điểm bằng ngưỡng. Những giá trị ở mép điều kiện là ca biên (boundary case); chúng giúp phát hiện nhầm > với >=.
def dem_diem_cao(diem):
dem = 0
for x in diem:
if x >= 8:
dem += 1
return dem
assert dem_diem_cao([]) == 0
assert dem_diem_cao([7]) == 0
assert dem_diem_cao([8]) == 1
print("Cac ca thu da qua")Cac ca thu da qua
assert kiểm tra một điều kiện mà bạn kỳ vọng là đúng. Nếu điều kiện đúng, chương trình tiếp tục. Nếu sai, Python phát sinh AssertionError:
so_ban = 0
assert so_ban > 0, "So ban phai duong"
print(24 / so_ban)Lỗi khi chạy, ở dòng 2
AssertionError: So ban phai duong
Thông báo sau dấu phẩy giúp nhận biết giả định bị vi phạm; phép chia không chạy. assert hữu ích khi tự kiểm tra giả định trong lúc phát triển. Không dựa vào nó để xử lý đầu vào không hợp lệ: Python có thể tắt kiểm tra assert ở chế độ tối ưu. Hãy dùng điều kiện hoặc xử lý ngoại lệ cho dữ liệu người dùng.
Với dữ liệu sai, hàm có thể chủ động phát sinh ValueError bằng raise. Nơi gọi bắt lỗi và xử lý; assert chỉ dành cho giả định trong lúc phát triển:
def chia_deu(tong, so_nhom):
if so_nhom <= 0:
raise ValueError("So nhom phai duong")
return tong / so_nhom
try:
print(chia_deu(12, 0))
except ValueError as loi:
print(loi)So nhom phai duong
Một số ca thử đúng không chứng minh chương trình đúng với mọi đầu vào. Chọn ca đa dạng, tự tính đáp án và bổ sung ca khi tìm thấy lỗi.
Với một hàm nhận danh sách, có thể chọn thêm danh sách chỉ có một phần tử, mọi phần tử đều đạt và không phần tử nào đạt. Khi tìm được một ca làm chương trình sai, giữ lại ca đó để thử lại sau khi sửa. Cách này giúp phát hiện lỗi cũ quay trở lại khi bạn thay đổi chương trình.
Lỗi thường gặp
| Lỗi | Vì sao sai | Cách sửa |
|---|---|---|
| Chỉ đọc dòng đầu traceback | Có thể bỏ qua loại lỗi và nơi lỗi phát sinh | Đọc dòng cuối rồi lần theo vị trí phía trên |
Dùng except ValueError để bắt lỗi chia 0 | Phép chia 0 gây ZeroDivisionError | Xử lý đúng loại lỗi hoặc kiểm tra số chia |
Dùng biến chỉ được gán trong try sau khi chuyển đổi thất bại | Phép gán chưa hoàn tất | Dùng biến trong else hoặc gán giá trị phù hợp ở nhánh lỗi |
Nghĩ finally đã xử lý lỗi | finally chỉ chạy bước kết thúc | Dùng except phù hợp nếu cần xử lý lỗi |
Để print dò lỗi trong bài nộp | Đầu ra có thêm dòng ngoài yêu cầu | Bỏ dòng dò lỗi trước khi nộp |
| Chỉ thử giá trị giữa khoảng | Có thể bỏ sót lỗi ở ranh giới | Thử cả giá trị ngay dưới và đúng ngưỡng |
Dùng assert để kiểm tra dữ liệu người dùng | Kiểm tra này có thể bị tắt | Dùng if hoặc xử lý ngoại lệ |
Tóm tắt
- Traceback giúp tìm loại lỗi và vị trí liên quan; không phải kết quả sai nào cũng gây lỗi.
trythử thao tác;exceptxử lý loại lỗi được chỉ định;elsechạy khitrythành công;finallychạy khi rời khối.- In giá trị trung gian giúp tìm bước sai. Xóa dòng dò lỗi khi đầu ra phải đúng định dạng.
- Ca thử cần có kết quả mong đợi và giá trị biên.
assertkiểm tra giả định trong quá trình phát triển.