DHDataHọc
Trắc nghiệmBài 12/305 phút đọc+10 XP

Bài tập: phân loại điểm

Bạn sẽ học được

  • Ghép f-string, xử lý chuỗi, ép kiểu và điều kiện thành một chương trình hoàn chỉnh.
  • Viết bảng xếp loại điểm với các mốc 90/80/70/50.
  • Phân biệt "điểm không hợp lệ" với "điểm 0".
  • Xử lý đầu vào là chuỗi, số thực, None, chuỗi rỗng.
  • Tự kiểm tra bằng một bộ ca thử thay vì gõ tay từng lần.

Đề bài

Viết chương trình nhận tên và điểm (có thể là chuỗi từ bàn phím), in ra xếp loại:

ĐiểmXếp loại
Từ 90 đến 100A
Từ 80 đến dưới 90B
Từ 70 đến dưới 80C
Từ 50 đến dưới 70D
Dưới 50F

Điểm ngoài khoảng 0–100 hoặc không đọc được thành số phải báo "điểm không hợp lệ". Tên rỗng hiển thị là "bạn".

Khái niệm cốt lõi

Tách bài toán thành ba hàm nhỏ, mỗi hàm một việc:

code
def xep_loai(diem):
    if not 0 <= diem <= 100:
        return "Điểm không hợp lệ"
    if diem >= 90:
        return "A"
    if diem >= 80:
        return "B"
    if diem >= 70:
        return "C"
    if diem >= 50:
        return "D"
    return "F"

Sau khi đã loại điểm ngoài khoảng, các nhánh chỉ cần kiểm tra cận dưới vì nhánh trước đã chặn cận trên. if diem >= 80 đứng sau nhánh >= 90, nên nó chỉ nhận điểm dưới 90. Đây là cách viết "thác nước" — điều kiện hẹp nhất trước, rộng dần.

Có thể dùng elif thay cho các if liền nhau; kết quả giống hệt vì mỗi nhánh đều return. Dùng if liền khi các nhánh độc lập và thoát sớm; dùng elif khi muốn nhấn mạnh "chỉ một nhánh chạy".

Kiểm tra thứ tự bằng bảng:

code
for diem in [95, 90, 85, 80, 72, 70, 50, 49, 100, 0, -5, 105]:
    print(diem, xep_loai(diem))
code
95 A
90 A
85 B
80 B
72 C
70 C
50 D
49 F
100 A
0 F
-5 Điểm không hợp lệ
105 Điểm không hợp lệ

Hai mốc đáng chú ý: 90 là A (điều kiện >= 90), 49 là F (không thoả >= 50), 0 là F hợp lệ chứ không phải không hợp lệ. Nếu viết if not diem: thay cho kiểm tra khoảng, điểm 0 sẽ bị báo sai — đây chính là cái bẫy truthiness của bài trước.

Đọc điểm từ chuỗi

input() trả chuỗi, và dữ liệu thật có thể là "85", "89.5", " 70 ", "abc", "", thậm chí None. Hàm đọc phải trả None khi không đọc được, để phần sau phân biệt "không đọc được" với "đọc được số 0":

code
def doc_diem(raw):
    try:
        diem = float(raw)
    except (TypeError, ValueError):
        return None
    return diem

print(doc_diem("85"), doc_diem("89.5"), doc_diem(" 70 "))
print(doc_diem("abc"), doc_diem(""), doc_diem(None))
code
85.0 89.5 70.0
None None None

Vì sao bắt cả TypeError:

  • float("abc") ném ValueError.
  • float("") ném ValueError.
  • float(None) ném TypeError.

Nếu chỉ bắt ValueError, ca None sẽ làm chương trình dừng. Bắt cả hai là cách viết phòng thủ cho dữ liệu đến từ ngoài.

float() tự bỏ khoảng trắng hai đầu, nên " 70 " đọc được mà không cần .strip(). Nó cũng nhận "1e2" (bằng 100) và "85,5" sẽ lỗi vì dấu phẩy thập phân kiểu Việt không phải cú pháp Python — nếu dữ liệu dùng dấu phẩy, phải replace(",", ".") trước.

Ghép báo cáo

code
def bao_cao(name, raw):
    name = name.strip() or "bạn"
    diem = doc_diem(raw)
    if diem is None:
        return f"{name}: điểm không hợp lệ"
    return f"{name}: {diem:g} → {xep_loai(diem)}"

name.strip() or "bạn" xử lý tên rỗng hoặc toàn khoảng trắng. {diem:g} in số gọn: 85.0 thành 85, 89.5 giữ nguyên. Nếu dùng {diem} mặc định, kết quả sẽ là 85.0.

Chạy bộ ca thử:

code
cases = [
    ("An", "85"),
    ("Bình", "89.5"),
    ("Chi", "abc"),
    ("Dũng", ""),
    ("Em", " 70 "),
    ("Phúc", None),
    ("Giang", "100"),
    ("  ", "49"),
]
for name, raw in cases:
    print(bao_cao(name, raw))
code
An: 85 → B
Bình: 89.5 → B
Chi: điểm không hợp lệ
Dũng: điểm không hợp lệ
Em: 70 → C
Phúc: điểm không hợp lệ
Giang: 100 → A
bạn: 49 → F

Lưu ý "89.5" là B chứ không phải A: mốc A là 90, không phải 89. Đây là dạng lỗi lệch một đơn vị rất dễ xảy ra khi dùng > thay >=.

Bọc phần nhập bàn phím ở ngoài cùng để hàm vẫn kiểm thử được:

code
if __name__ == "__main__":
    name = input("Tên: ")
    raw = input("Điểm: ")
    print(bao_cao(name, raw))

Tự kiểm tra

Trả lời trước khi xem đáp án:

  1. Điểm 90 thuộc loại nào? 89.9 thuộc loại nào?
  2. Vì sao phải kiểm tra khoảng 0 <= diem <= 100 trước các nhánh xếp loại?
  3. float(None) ném lỗi gì? Vì sao phải bắt cả TypeError?
  4. doc_diem trả None thay vì 0 khi lỗi — lợi ích là gì?
  5. Vì sao {diem:g} tốt hơn {diem} trong báo cáo?
  6. Nếu bỏ nhánh kiểm tra khoảng, điểm 105 sẽ ra loại gì?

Đáp án:

  1. 90 là A; 89.9 là B.
  2. Để điểm âm hoặc trên 100 không bị xếp vào A/F như điểm hợp lệ.
  3. TypeError; float("abc") là ValueError, còn None sai kiểu hoàn toàn.
  4. None nghĩa là "không có dữ liệu", phân biệt được với điểm 0 hợp lệ.
  5. :g bỏ phần .0 thừa, giữ 89.5 nguyên vẹn.
  6. 105 >= 90 nên sẽ ra A — sai hoàn toàn.

Thực hành thêm

  1. Đổi bảng xếp loại sang thang 10 (9/8/7/5).
  2. Nhận thêm hệ số chuyên cần và hạ một bậc nếu dưới 0.8.
  3. Dùng match thay if/elif cho các mốc điểm và so sánh độ đọc hiểu.
  4. Thêm ca thử "-5", "105", "1e2", "85,5" và dự đoán kết quả trước khi chạy.
  5. In báo cáo cho cả lớp từ danh sách (ten, diem) và đếm số lượng mỗi loại bằng dict.

Lỗi thường gặp

Triệu chứngNguyên nhânCách sửa
Điểm 0 bị báo không hợp lệDùng if not diemKiểm tra is None và khoảng
105 ra loại AKhông chặn cận trênKiểm tra 0 <= diem <= 100
89.5 ra ADùng > thay >= hoặc sai mốcRà lại bảng mốc
Chương trình dừng ở NoneChỉ bắt ValueErrorBắt thêm TypeError
85.0 hiện trong báo cáoThiếu format spec{diem:g}
"85,5" lỗiDấu phẩy thập phânreplace(",", ".") trước
Tên rỗng hiện trốngKhông xử lý chuỗi rỗngname.strip() or "bạn"

Tóm tắt

  • Xếp loại bằng thác nước điều kiện: chặn khoảng trước, rồi so cận dưới.
  • 0 là điểm hợp lệ; None mới là "không đọc được".
  • float() ném ValueError hoặc TypeError — bắt cả hai.
  • Tách hàm nhỏ để chạy được bộ ca thử không cần bàn phím.
  • :g giữ báo cáo gọn; >= quyết định đúng các mốc biên.

Dùng ← → để chuyển bài