Bài 29: Thực hành thiết kế chương trình theo mô đun

I. Ôn tập kiến thức nền

Bài thực hành này vận dụng phương pháp thiết kế theo mô đun đã học ở Bài 28 để xây dựng một chương trình hoàn chỉnh. Nhắc lại các ý chính:

Khái niệm Nội dung
Mô đun Một chương trình con (hàm) đảm nhận chức năng riêng biệt, độc lập
Cách chia mô đun Thường chia theo 3 nhóm: nhập dữ liệu – xử lí dữ liệu – xuất kết quả
Chương trình chính Ghép nối các mô đun theo đúng thứ tự
Lợi ích Dễ hiểu, dễ sửa, dễ bổ sung, hỗ trợ làm việc nhóm

Ngoài ra, bài thực hành còn sử dụng kỹ thuật đọc/ghi tệpsắp xếp song song hai mảng đã quen thuộc từ các bài trước.

II. Hướng dẫn thực hành

Nhiệm vụ: Tính điểm tổng hợp vận động viên Sea Games

Yêu cầu bài toán

Trong một cuộc thi thể thao dạng Sea Games, ban tổ chức (BTC) lưu kết quả chấm điểm vào tệp SeaGames.inp. Mỗi dòng gồm: mã số vận động viên (VĐV), tiếp theo là điểm của các giám khảo (cách nhau bằng dấu cách). Số lượng giám khảo có thể khác nhau ở các bộ môn khác nhau.

Quy định tính điểm tổng hợp: Điểm tổng hợp = trung bình cộng điểm ban giám khảo, nhưng giám khảo đầu tiên và giám khảo cuối cùnghệ số 2 (tính điểm hai lần), các giám khảo còn lại hệ số 1.

Yêu cầu: Tính điểm tổng hợp cho từng VĐV, ghi ra tệp ketqua.out (mỗi dòng gồm mã VĐV và điểm tổng hợp, làm tròn 2 chữ số thập phân).

Dữ liệu mẫu:

SeaGames.inp                     ketqua.out
101 7.5 8.0 9.0 9.5 7.1 6.8      101 7.77
200 8.5 9.1 9.5 8.6 9.9          200 9.14
003 6.6 7.0 7.5 6.8 5.9 8.1      003 7.08
045 8.5 7.9 9.3 9.0 8.9          045 8.71
901 9.2 9.7 8.6 9.0              901 9.06

Phân tích tổng quát: chia mô đun

Bài toán được chia thành 3 mô đun độc lập:

Mô đun Chức năng Hàm
Nhập dữ liệu Đọc tệp, tách mã VĐV và dãy điểm nhapDL(finp)
Xử lí dữ liệu Tính điểm tổng hợp cho từng VĐV Xuly(Diem) + diem_gk(d)
Xuất kết quả Ghi mã VĐV và điểm tổng hợp ra tệp ghiDL(fout, DS, kq)

Chương trình chính chỉ gồm 5 dòng ghép nối:

finp = "SeaGames.inp"
fout = "ketqua.out"
DS, Diem = nhapDL(finp)
kq = Xuly(Diem)
ghiDL(fout, DS, kq)

Giờ ta thiết kế chi tiết từng mô đun.

Mô đun 1 – Nhập dữ liệu: nhapDL(finp)

Hàm đọc tệp finp, tách mỗi dòng thành hai phần: phần tử đầu tiên là mã VĐV (đưa vào mảng DS), các phần tử còn lại là dãy điểm (chuyển thành số thực, đưa vào mảng Diem).

def nhapDL(finp):
    f = open(finp)
    A = []
    B = []
    for line in f:
        S = line.split()
        A.append(S[0])
        temp = S[1:len(S)]
        temp = [float(x) for x in temp]
        B.append(temp)
    f.close()
    return A, B

Giải thích chi tiết:

  • Dòng 6: line.split() tách dòng theo dấu cách. Ví dụ dòng "101 7.5 8.0 9.0 9.5 7.1 6.8" → S = ['101', '7.5', '8.0', '9.0', '9.5', '7.1', '6.8'].
  • Dòng 7: S[0] = '101' (mã VĐV) → thêm vào mảng A.
  • Dòng 8: S[1:len(S)] = ['7.5', '8.0', '9.0', '9.5', '7.1', '6.8'] (phần điểm, vẫn là chuỗi).
  • Dòng 9: List comprehension [float(x) for x in temp] chuyển tất cả thành số thực → [7.5, 8.0, 9.0, 9.5, 7.1, 6.8].
  • Dòng 10: Thêm dãy điểm vào mảng B.

Kết quả sau khi đọc:

DS = ['101', '200', '003', '045', '901']
Diem = [[7.5, 8.0, 9.0, 9.5, 7.1, 6.8],
         [8.5, 9.1, 9.5, 8.6, 9.9],
         [6.6, 7.0, 7.5, 6.8, 5.9, 8.1],
         [8.5, 7.9, 9.3, 9.0, 8.9],
         [9.2, 9.7, 8.6, 9.0]]

Lưu ý: mảng Diemmảng hai chiều (danh sách các danh sách). Mỗi VĐV có dãy điểm dài ngắn khác nhau tuỳ bộ môn.

Mô đun 2 – Xử lí dữ liệu: diem_gk(d)Xuly(Diem)

Mô đun này cần hai hàm: một hàm tính điểm tổng hợp cho một VĐV, một hàm áp dụng cho tất cả VĐV.

Hàm diem_gk(d) – tính điểm tổng hợp từ dãy điểm:

def diem_gk(d):
    diem = sum(d) + d[0] + d[len(d) - 1]
    diem = diem / (len(d) + 2)
    return round(diem, 2)

Giải thích công thức tính:

Giám khảo đầu (d[0]) và cuối (d[len(d)-1]) có hệ số 2, các giám khảo còn lại hệ số 1. Cách tính:

  • sum(d) = tổng tất cả điểm (mỗi giám khảo tính 1 lần).
  • Cộng thêm d[0] + d[len(d)-1] = tính thêm lần thứ 2 cho giám khảo đầu và cuối.
  • Tổng hệ số = len(d) + 2 (len(d) giám khảo × hệ số 1, cộng thêm 2 cho hai giám khảo đặc biệt).
  • Chia để lấy trung bình, làm tròn 2 chữ số.

Minh hoạ với VĐV 101, dãy điểm [7.5, 8.0, 9.0, 9.5, 7.1, 6.8]:

sum(d) = 7.5 + 8.0 + 9.0 + 9.5 + 7.1 + 6.8 = 47.9
Cộng thêm: d[0] + d[5] = 7.5 + 6.8 = 14.3
Tổng = 47.9 + 14.3 = 62.2
Số hệ số = 6 + 2 = 8
Điểm tổng hợp = 62.2 / 8 = 7.775 → làm tròn = 7.77 ✓

Hàm Xuly(Diem) – tính cho tất cả VĐV:

def Xuly(B):
    kq = []
    for i in range(len(B)):
        diem = diem_gk(B[i])
        kq.append(diem)
    return kq

Giải thích: Duyệt từng phần tử B[i] (dãy điểm của VĐV thứ i), gọi diem_gk() để tính, thêm kết quả vào mảng kq. Kết quả: kq = [7.77, 9.14, 7.08, 8.71, 9.06].

Mô đun 3 – Xuất kết quả: ghiDL(fout, DS, kq)

def ghiDL(fout, A, B):
    f = open(fout, "w")
    for i in range(len(A)):
        print(A[i], B[i], file=f)
    f.close()

Giải thích: Mở tệp fout ở chế độ ghi ("w"), duyệt từng VĐV, in mã số và điểm tổng hợp trên cùng dòng. Kết quả tệp ketqua.out:

101 7.77
200 9.14
003 7.08
045 8.71
901 9.06

Chương trình hoàn chỉnh

# SeaGames.py

def nhapDL(finp):
    f = open(finp)
    A = []
    B = []
    for line in f:
        S = line.split()
        A.append(S[0])
        temp = S[1:len(S)]
        temp = [float(x) for x in temp]
        B.append(temp)
    f.close()
    return A, B

def diem_gk(d):
    diem = sum(d) + d[0] + d[len(d) - 1]
    diem = diem / (len(d) + 2)
    return round(diem, 2)

def Xuly(B):
    kq = []
    for i in range(len(B)):
        diem = diem_gk(B[i])
        kq.append(diem)
    return kq

def ghiDL(fout, A, B):
    f = open(fout, "w")
    for i in range(len(A)):
        print(A[i], B[i], file=f)
    f.close()

# Chương trình chính
finp = "SeaGames.inp"
fout = "ketqua.out"
DS, Diem = nhapDL(finp)
kq = Xuly(Diem)
ghiDL(fout, DS, kq)

Chương trình gồm 4 hàm (nhapDL, diem_gk, Xuly, ghiDL) và phần chương trình chính 5 dòng. Mỗi hàm có thể kiểm tra, sửa lỗi hoặc thay thế độc lập mà không ảnh hưởng đến các hàm còn lại.

III. Phương pháp giải bài tập SGK

Bài Luyện tập

Bài 1 (Luyện tập SGK)

Hãy chỉnh sửa lại chương trình trên nếu bổ sung thêm điều kiện sau vào nhiệm vụ: Trong tệp kết quả đầu ra, thứ tự các vận động viên được ghi theo thứ tự giảm dần của điểm đánh giá.

Lời giải:

Cần thêm bước sắp xếp trước khi ghi kết quả. Sắp xếp mảng kq (điểm tổng hợp) giảm dần, đồng thời đổi chỗ tương ứng trong mảng DS (mã VĐV) – kỹ thuật sắp xếp song song hai mảng.

Thêm một hàm sắp xếp:

def SapxepGiam(A, B):
    """Sắp xếp A giảm dần, đổi chỗ B tương ứng"""
    n = len(A)
    for i in range(1, n):
        j = i
        while j > 0 and A[j] > A[j - 1]:    # Đổi < thành > để giảm dần
            A[j], A[j - 1] = A[j - 1], A[j]
            B[j], B[j - 1] = B[j - 1], B[j]
            j = j - 1

Chương trình chính chỉ cần thêm một dòng trước khi ghi:

finp = "SeaGames.inp"
fout = "ketqua.out"
DS, Diem = nhapDL(finp)
kq = Xuly(Diem)
SapxepGiam(kq, DS)          # Dòng thêm mới
ghiDL(fout, DS, kq)

Kết quả tệp ketqua.out sau khi sắp xếp:

200 9.14
901 9.06
045 8.71
101 7.77
003 7.08

Giải thích: Chỉ cần thêm một mô đun sắp xếp mới, không sửa bất kỳ mô đun nào đã có (nhapDL, Xuly, ghiDL giữ nguyên). Lưu ý gọi SapxepGiam(kq, DS) – sắp xếp theo kq (điểm), đổi chỗ DS (mã) tương ứng.

Bài 2 (Luyện tập SGK)

Trong nhiệm vụ trên, nếu công thức tính điểm tổng hợp của Sea Games thay đổi thì chúng ta có phải sửa lại toàn bộ chương trình hay không? Nếu cần thì chỉ phải sửa mô đun nào? Hàm nào?

Lời giải:

Không cần sửa lại toàn bộ chương trình. Chỉ cần sửa mô đun xử lí dữ liệu, cụ thể là hàm diem_gk(d) – hàm chứa công thức tính điểm tổng hợp.

Các mô đun còn lại hoàn toàn không bị ảnh hưởng vì:

  • nhapDL() chỉ lo đọc dữ liệu thô → không liên quan đến cách tính điểm.
  • Xuly() gọi diem_gk() nên tự động áp dụng công thức mới → không cần sửa.
  • ghiDL() chỉ lo ghi kết quả → không liên quan.

Đây chính là lợi ích cốt lõi của thiết kế mô đun: khi yêu cầu thay đổi, chỉ cần xác định mô đun nào chịu trách nhiệm và sửa cục bộ.

Bài Vận dụng

Bài 1 (Vận dụng SGK)

Cho trước số tự nhiên n, cần in ra trên màn hình dãy n số nguyên tố đầu tiên. Ví dụ nếu n = 5 thì dãy cần in ra sẽ là 2, 3, 5, 7, 11.

Lời giải:

Phân tích mô đun:

Mô đun Chức năng Hàm
Kiểm tra nguyên tố Kiểm tra một số có phải nguyên tố không la_nguyen_to(k)
Tìm n số nguyên tố Tìm và lưu n số nguyên tố đầu tiên tim_n_nguyen_to(n)
Xuất kết quả In dãy số nguyên tố ra màn hình Phần chương trình chính

Chương trình:

def la_nguyen_to(k):
    """Kiểm tra k có phải số nguyên tố không"""
    if k < 2:
        return False
    for i in range(2, int(k**0.5) + 1):
        if k % i == 0:
            return False
    return True

def tim_n_nguyen_to(n):
    """Tìm n số nguyên tố đầu tiên, trả về danh sách"""
    ds = []
    k = 2
    while len(ds) < n:
        if la_nguyen_to(k):
            ds.append(k)
        k = k + 1
    return ds

# Chương trình chính
n = int(input("Nhập n: "))
ketqua = tim_n_nguyen_to(n)
print(ketqua)

Kết quả với n = 5: [2, 3, 5, 7, 11]

Giải thích: Hàm la_nguyen_to(k) kiểm tra số k bằng cách duyệt ước từ 2 đến √k. Hàm tim_n_nguyen_to(n) bắt đầu từ k = 2, kiểm tra từng số, nếu là nguyên tố thì thêm vào danh sách, dừng khi đủ n số. Hai hàm độc lập: nếu muốn đổi cách kiểm tra nguyên tố (ví dụ dùng thuật toán nhanh hơn), chỉ cần sửa la_nguyen_to().

Bài 2 (Vận dụng SGK)

Trong một kì thi Tin học trẻ, mỗi học sinh sẽ phải làm 3 bài thi. Với mỗi bài, nếu học sinh làm sẽ được ban giám khảo chấm và cho điểm, nếu không làm thì sẽ không tính điểm. Sau khi thi, dữ liệu điểm thi của học sinh sẽ được lưu trong một tệp văn bản và gửi về ban tổ chức. Mẫu một tệp điểm thi có dạng sau:

Diemthi.inp

A12 12 -1 15
B123 9 14 -1
C11 10 12 18
A110 10 -1 -1
B01 12 10 4

Quy định ghi trong tệp trên như sau:

  • Mỗi dòng sẽ bắt đầu bằng số báo danh của thí sinh, tiếp theo là ba giá trị điểm tương ứng với ba bài thi.
  • Điểm thi sẽ là một số tự nhiên từ 0 đến 20.
  • Nếu học sinh không làm thì bài đó ghi -1.

Em có nhiệm vụ tính toán tổng số điểm thi của các bạn học sinh và đưa dữ liệu ra tệp ketqua.out là danh sách ba bạn có tổng điểm cao nhất được sắp xếp giảm dần từ trên xuống dưới. Khuôn dạng dữ liệu đưa ra bao gồm: số báo danh, các điểm thi từng bài và cuối cùng là tổng điểm của cả ba bài.

Ví dụ với dữ liệu trên thì kết quả như sau:

ketqua.out

C11 10 12 18 40
A12 12 -1 15 27
B01 12 10 4 26

Lời giải:

Phân tích mô đun:

Mô đun Chức năng Hàm
Nhập dữ liệu Đọc tệp, tách SBD và dãy điểm nhapDL(finp)
Tính tổng điểm Tính tổng (bỏ qua bài −1) tinh_tong(diem)
Xử lí chính Tính tổng cho tất cả + sắp xếp giảm dần Xuly(DS, Diem)
Xuất kết quả Ghi 3 bạn đầu tiên ra tệp ghiDL(fout, DS, Diem, Tong)

Chương trình:

def nhapDL(finp):
    """Đọc tệp, trả về mảng SBD và mảng điểm"""
    f = open(finp)
    DS = []
    Diem = []
    for line in f:
        S = line.split()
        DS.append(S[0])
        d = [int(x) for x in S[1:]]
        Diem.append(d)
    f.close()
    return DS, Diem

def tinh_tong(diem):
    """Tính tổng điểm, bài không làm (giá trị -1) không tính"""
    tong = 0
    for d in diem:
        if d != -1:
            tong = tong + d
    return tong

def SapxepGiam(A, B, C):
    """Sắp xếp A giảm dần, đổi chỗ B và C tương ứng"""
    n = len(A)
    for i in range(1, n):
        j = i
        while j > 0 and A[j] > A[j - 1]:
            A[j], A[j - 1] = A[j - 1], A[j]
            B[j], B[j - 1] = B[j - 1], B[j]
            C[j], C[j - 1] = C[j - 1], C[j]
            j = j - 1

def Xuly(DS, Diem):
    """Tính tổng điểm cho mọi HS, sắp xếp giảm dần theo tổng"""
    Tong = []
    for i in range(len(Diem)):
        Tong.append(tinh_tong(Diem[i]))
    SapxepGiam(Tong, DS, Diem)
    return Tong

def ghiDL(fout, DS, Diem, Tong, so_luong=3):
    """Ghi ra tệp: số lượng bạn đầu tiên (mặc định 3)"""
    f = open(fout, "w")
    for i in range(so_luong):
        dong = DS[i]
        for d in Diem[i]:
            dong = dong + " " + str(d)
        dong = dong + " " + str(Tong[i])
        print(dong, file=f)
    f.close()

# Chương trình chính
finp = "Diemthi.inp"
fout = "ketqua.out"
DS, Diem = nhapDL(finp)
Tong = Xuly(DS, Diem)
ghiDL(fout, DS, Diem, Tong)

Giải thích chi tiết từng mô đun:

nhapDL(finp): Tương tự bài Sea Games nhưng đơn giản hơn – mỗi dòng luôn có đúng 4 phần tử (SBD + 3 điểm). Điểm được chuyển thành int vì đề bài nói điểm là số tự nhiên.

tinh_tong(diem): Duyệt dãy 3 điểm, cộng dồn những bài có điểm ≥ 0 (bỏ qua bài ghi −1). Ví dụ: A12 có điểm [12, −1, 15] → tổng = 12 + 15 = 27.

SapxepGiam(A, B, C): Sắp xếp mảng A (tổng điểm) giảm dần, đồng thời đổi chỗ cả B (SBD) và C (dãy điểm chi tiết). Vì bài này có 3 mảng song song nên hàm sắp xếp cần đổi chỗ cả 3.

Xuly(DS, Diem): Tính tổng cho mọi HS rồi gọi sắp xếp. Sau khi chạy, cả 3 mảng DS, Diem, Tong đều đã được sắp xếp theo tổng điểm giảm dần.

ghiDL(...): Ghi so_luong bạn đầu tiên (mặc định 3) ra tệp. Mỗi dòng gồm: SBD + từng điểm + tổng.

Kết quả:

C11 10 12 18 40
A12 12 -1 15 27
B01 12 10 4 26
Thầy Phạm Thành Danh

Thầy Phạm Thành Danh

(Người kiểm duyệt, ra đề)

Chức vụ: Trưởng ban biên soạn môn Tin Học THPT

Trình độ: Cử nhân Sư phạm Tin học, Chứng chỉ hạng II, Chứng chỉ Tin học, Ngoại ngữ B1

Kinh nghiệm: 8+ năm kinh nghiệm tại Trường THPT Thuận Hóa