Chương 0·Bài 0.1

Python

Bài Python dạy cách viết hàm, chọn cấu trúc dữ liệu, và giữ dữ liệu đúng khi chương trình chạy lại. Bốn ví dụ dưới đây đi từ một phép lọc điểm đơn giản tới việc đọc, kiểm tra và ghi lại một file CSV. Tên và điểm trong các ví dụ đều là dữ liệu minh họa.

Ví dụ 1: Thay ngưỡng điểm, danh sách đạt đổi thế nào?

Có bốn điểm trên thang 10 là 4.5, 6.0, 8.0, 5.0. Ta cần tính điểm trung bình, và lấy ra những điểm từ ngưỡng trở lên. Làm hai lần, lần đầu với ngưỡng 5, lần sau với ngưỡng 6.

Python
def summarize_scores(values, threshold=5.0):
    if not values:
        raise ValueError("danh sách điểm rỗng")
    passed = []
    for value in values:
        if value >= threshold:
            passed.append(value)
    # Gom đủ các điểm rồi mới trả về. Đặt return trong vòng for sẽ bỏ sót các điểm phía sau.
    return sum(values) / len(values), passed

scores = [4.5, 6.0, 8.0, 5.0]
for threshold in [5.0, 6.0]:
    mean_score, passed = summarize_scores(scores, threshold=threshold)
    print(f"ngưỡng {threshold:.1f}: trung bình {mean_score:.3f}, đạt {passed}")

try:
    summarize_scores([])
except ValueError as error:
    print("ValueError:", error)

Tổng điểm là 23.5, nên trung bình luôn là 23.5 / 4 = 5.875, dù ngưỡng là bao nhiêu. Đổi ngưỡng chỉ làm đổi phép lọc: điểm 5.0 được giữ ở lần đầu, và bị loại ở lần sau. Dòng kiểm danh sách rỗng chặn phép chia cho 0 ngay từ đầu hàm, đúng chỗ ta biết rõ nguyên nhân để báo lỗi.

Ví dụ 2: Sửa thử một bảng mà vẫn giữ được bảng gốc

Bảng có hai hàng là hai học sinh, hai cột là điểm Toán và điểm Tin. Ta muốn thử đổi ô đầu tiên từ 6 thành 10, nhưng vẫn giữ được điểm gốc. Hãy xem phép gán thường và phép sao chép một tầng có làm được việc đó không.

Python
from copy import deepcopy

original = [[6, 8], [5, 7]]
trial = original
trial[0][0] = 10
print("gán tên:", original, "cùng đối tượng:", trial is original)

original = [[6, 8], [5, 7]]
trial = original.copy()
trial[0][0] = 10
print("copy một tầng:", original, "cùng hàng:", trial[0] is original[0])

original = [[6, 8], [5, 7]]
trial = deepcopy(original)
trial[0][0] = 10
print("deepcopy:", original, trial)
assert original == [[6, 8], [5, 7]]

# Mỗi hàng của bảng kết quả phải là một list mới, không dùng chung.
table_bad = [[0, 0]] * 2
table_good = [[0, 0] for _ in range(2)]
table_bad[0][1] = table_good[0][1] = 9
print("hàng dùng chung:", table_bad)
print("hàng độc lập:", table_good)

Ở hai lần đầu, original[0][0] cũng bị đổi thành 10. Lần thứ nhất, hai tên trỏ vào cùng một bảng. Lần thứ hai, bảng mới là bản sao, nhưng các hàng con bên trong vẫn là của bảng cũ. Chỉ deepcopy mới giữ được bảng gốc. Hai bảng cuối đều có kích thước 2 × 2, nhưng chỉ bảng tạo từng hàng riêng mới đổi đúng một ô. Bảng [[0, 0]] * 2 thật ra là cùng một hàng được nhắc lại hai lần.

Ví dụ 3: Đánh mã cho từ vựng sao cho chạy lại vẫn ra như cũ

Dãy từ mèo, chó, mèo, chim cần được đổi thành các ID nguyên. Yêu cầu là cùng một tập từ thì phải ra cùng một bảng mã, dù thứ tự xuất hiện có đổi. Từ chưa gặp bao giờ sẽ nhận mã của <UNK>, là 0.

Python
from collections import Counter

def make_vocab(tokens):
    # Bỏ từ trùng, để <UNK> ở vị trí 0, rồi sắp xếp các từ còn lại cho thứ tự luôn cố định.
    words = ["<UNK>"] + sorted(set(tokens) - {"<UNK>"})
    return {word: index for index, word in enumerate(words)}

tokens = ["mèo", "chó", "mèo", "chim"]
vocab = make_vocab(tokens)
encoded = [vocab[token] for token in tokens]
print("bảng mã:", vocab)
print("dãy ID:", encoded)
print("tần suất:", sorted(Counter(tokens).items()))
print("đảo dữ liệu, giữ bảng mã:", vocab == make_vocab(tokens[::-1]))
assert make_vocab(["<UNK>", "mèo"])["<UNK>"] == 0

new_tokens = ["chó", "cá"]
new_ids = [vocab.get(token, vocab["<UNK>"]) for token in new_tokens]
print("từ mới:", new_ids)

Hai lần xuất hiện của mèo đều nhận ID 3, và đảo ngược dãy đầu vào thì bảng mã vẫn y nguyên. Nếu không sắp xếp, thứ tự của set có thể khác giữa các lần chạy, và cùng một từ sẽ nhận mã khác nhau. Từ cá chưa có trong từ vựng nên nhận 0, thay vì làm chương trình dừng với KeyError. Ví dụ này coi các từ đã được tách sẵn, chưa bàn chuyện tách một câu thành từ.

Ví dụ 4: Đọc điểm từ CSV, ghi lại lỗi, rồi kiểm file đã ghi

File CSV nhỏ dưới đây có ba chỗ dễ làm hỏng dữ liệu: một điểm ghi ?, một ID bắt đầu bằng số 0, và một tên có dấu phẩy bên trong. Ta cần giữ nguyên ID và tên, báo đúng dòng nào chưa đọc được điểm, rồi ghi ra những dòng đạt từ 6 điểm trở lên, sắp theo điểm giảm dần.

Python
import csv
from pathlib import Path
from tempfile import TemporaryDirectory

def read_scores(path):
    valid, problems = [], []
    with path.open(encoding="utf-8", newline="") as file:
        for row in csv.DictReader(file):
            try:
                score = float(row["score"])
            except ValueError:
                problems.append((row["id"], "điểm không phải số"))
                continue
            if not 0 <= score <= 10:
                problems.append((row["id"], "điểm ngoài thang 0-10"))
                continue
            valid.append({"id": row["id"], "name": row["name"], "score": score})
    return valid, problems

with TemporaryDirectory(prefix="python-csv-", dir=".") as directory:
    data_dir = Path(directory)
    source = data_dir / "scores.csv"
    source.write_text(
        'id,name,score\n001,An,8.5\n002,"Bình, lớp A",6.0\n'
        '003,Chi,?\n004,Dũng,4.5\n', encoding="utf-8")
    rows, problems = read_scores(source)
    print("lỗi cần kiểm:", problems)
    print("tên có dấu phẩy:", rows[1]["name"])

    selected = sorted((row for row in rows if row["score"] >= 6),
                      key=lambda row: -row["score"])
    assert len({row["id"] for row in selected}) == len(selected)
    destination = data_dir / "accepted.csv"
    with destination.open("w", encoding="utf-8", newline="") as file:
        writer = csv.DictWriter(file, fieldnames=["id", "name", "score"])
        writer.writeheader()
        writer.writerows(selected)

    # Đọc lại chính file vừa ghi để kiểm, chứ không chỉ kiểm biến selected trong bộ nhớ.
    restored, restored_problems = read_scores(destination)
    assert restored == selected and not restored_problems
    print("đọc lại:", [(row["id"], row["score"]) for row in restored])
    print("CSV giữ đúng dữ liệu:", restored == selected)

ID 003 nằm trong danh sách lỗi, chứ không bị lặng lẽ biến thành điểm 0. Tên Bình, lớp A vẫn là một ô, vì module csv hiểu dấu ngoặc kép bao quanh. Khi đọc lại file đã ghi, 001 vẫn giữ số 0 ở đầu, và hai điểm đạt là 8.5 với 6.0. Thư mục tạm tự được dọn sau khi kiểm xong. Đây cũng là thói quen nên giữ khi đi thi: ghi file nộp xong thì đọc lại chính file ấy một lần.

Mục lục