← Lập trình Python nâng cao

Bài 9 · Nâng cao · 22 phút· Cập nhật 11/06/2026

Hệ thống tệp trong Python

Biên soạn bởi Nguyễn Anh Tuấn

Làm việc với tệp trong Python: pathlib & os, context manager (with), đọc/ghi văn bản & nhị phân, JSON/CSV - và cách ghi file an toàn.

pathlib.Path ghép đường dẫn đúng trên mọi OS (toán tử /) và mang sẵn nhiều tiện ích - tìm kiếm, thông tin file:

paths.py

from pathlib import Path

thu_muc = Path("data"); thu_muc.mkdir(exist_ok=True)
tep = thu_muc / "ghi_chu.txt"
tep.write_text("Xin chao", encoding="utf-8")

print(tep.exists(), tep.suffix, tep.stem, tep.stat().st_size)
print([p.name for p in thu_muc.glob("*.txt")])     # tim 1 cap
# thu_muc.rglob("*.py")  -> tim DE QUY moi cap con

Kết quả khi chạy

True .txt ghi_chu 8
['ghi_chu.txt']
  • Path("data") / "a.txt" ghép đúng trên Windows & Unix.
  • glob("*.txt") tìm một cấp; rglob("*.txt") tìm đệ quy; cả hai trả iterator lười.
  • .suffix/.stem/.parent/.stat() cho đuôi/tên/thư mục cha/kích thước - khỏi tự cắt str.

with open(...) là một context manager: mở file ở __enter__luôn đóng__exit__ - kể cả khi có lỗi. Bấm thử kịch bản "Có lỗi":

1 with open("nhat_ky.txt", "w") as f:
2 f.write("dong 1")
3 print("xong")
Trạng thái file ĐANG MỞ
Nhật ký vòng đời
  • __enter__ → mở file
bước 1/4
with gọi __enter__: mở file, gán cho f.

rw.py

with open("nhat_ky.txt", "w", encoding="utf-8") as f:
    f.write("dong 1\n"); f.write("dong 2\n")

with open("nhat_ky.txt", encoding="utf-8") as f:
    for dong in f:               # doc tung dong (luoi, it bo nho)
        print(dong.rstrip())

Kết quả khi chạy

dong 1
dong 2
  • with open(...) as f: rời khối là file TỰ ĐÓNG - kể cả khi giữa chừng có lỗi.
  • for dong in f đọc TỪNG DÒNG (lười) - hợp file lớn.
  • Luôn nêu encoding="utf-8" để tiếng Việt không lỗi font.

with không chỉ cho file - bạn tự viết được context manager để “mở rồi chắc chắn dọn” (kết nối, khoá, thư mục tạm…). Hai cách:

ctx.py

# Cach 1: class voi __enter__/__exit__
class Mo:
    def __enter__(self):
        print("mo tai nguyen"); return "r"
    def __exit__(self, exc_type, exc, tb):
        print("don dep")          # chay du co loi hay khong

# Cach 2 (gon hon): @contextmanager tren mot generator
from contextlib import contextmanager

@contextmanager
def mo():
    print("setup")               # truoc yield = __enter__
    try:
        yield 42                 # gia tri gan cho "as"
    finally:
        print("teardown")        # sau yield = __exit__ (luon chay)

with mo() as x:
    print("dung", x)

Kết quả khi chạy

setup
dung 42
teardown
  • Class: __enter__ (mở, trả thứ cho “as”) + __exit__ (dọn, chạy kể cả khi lỗi).
  • @contextmanager: trước yield = setup, sau yield (đặt trong finally) = teardown - gọn hơn.
  • Dùng cho mọi cặp “mở/đóng” có kỷ luật: kết nối DB, khoá, đổi cwd, đo thời gian…

json_io.py

import json
nguoi = {"ten": "An", "diem": [8, 9, 10], "dat": True}

with open("nguoi.json", "w", encoding="utf-8") as f:
    json.dump(nguoi, f, ensure_ascii=False, indent=2)   # giu tieng Viet

with open("nguoi.json", encoding="utf-8") as f:
    lai = json.load(f)
print(lai["ten"], lai == nguoi)

Kết quả khi chạy

An True
  • json.dump/load với tệp; json.dumps/loads với str (gửi qua mạng).
  • ensure_ascii=False giữ tiếng Việt; indent=2 cho dễ đọc.
  • JSON hợp dữ liệu lồng nhau (dict/list).

csv_io.py

import csv
rows = [{"ten": "An", "diem": 8}, {"ten": "Binh", "diem": 9}]

with open("diem.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.DictWriter(f, fieldnames=["ten", "diem"])
    w.writeheader(); w.writerows(rows)

with open("diem.csv", newline="", encoding="utf-8") as f:
    doc = list(csv.DictReader(f))
print(doc[0]["ten"], len(doc))

Kết quả khi chạy

An 2
  • DictWriter/DictReader làm việc theo TÊN CỘT - rõ ràng, ít lỗi thứ tự.
  • Nhớ newline="" khi mở file CSV (tránh dòng trống thừa trên Windows).
  • CSV hợp xuất Excel/bảng; JSON hợp dữ liệu lồng nhau.

Chế độ nhị phân ("rb"/"wb") cho dữ liệu thô (ảnh, zip). Và một mẫu “pro” quan trọng: ghi an toàn - đừng đè trực tiếp lên file quan trọng:

atomic.py

import os
from pathlib import Path

def ghi_an_toan(path: str, text: str):
    p = Path(path)
    tmp = p.with_suffix(p.suffix + ".tmp")
    tmp.write_text(text, encoding="utf-8")   # ghi ra file TAM truoc
    os.replace(tmp, p)                        # doi ten NGUYEN TU -> an toan

ghi_an_toan("config.json", '{"ok": true}')
  • Chế độ văn bản ("r"/"w") cho str (qua encoding); nhị phân ("rb"/"wb") cho bytes thô (ảnh/zip).
  • Mở ảnh bằng chế độ văn bản sẽ HỎNG dữ liệu - chọn đúng chế độ.
  • Ghi an toàn: ghi file tạm rồi os.replace → người đọc luôn thấy bản hoàn chỉnh, không bao giờ file dở.

Tiếp theo

Tệp hợp dữ liệu nhỏ. Khi cần truy vấn & toàn vẹn, ta cần cơ sở dữ liệu - bài kế tiếp: SQLite, transaction, tham số hoá chống injection, và hiệu năng (index).

Câu hỏi thường gặp

Nối str dễ sai (thiếu/thừa /, khác nhau Windows \ vs Unix /). pathlib dùng toán tử / ghép đúng trên mọi OS, và có sẵn .exists(), .glob(), .suffix, .stem, .parent, .stat()… Code gọn & ít lỗi.

p.glob("*.txt") tìm trong MỘT cấp thư mục; p.rglob("*.txt") (hoặc glob("**/*.txt")) tìm ĐỆ QUY mọi cấp con. Cả hai trả về một iterator các Path (lười) - duyệt bằng for hoặc bọc list().

open() chiếm một file handle của OS; không đóng có thể rò rỉ tài nguyên hoặc mất dữ liệu chưa kịp ghi. with ĐẢM BẢO đóng khi rời khối - kể cả khi giữa chừng có lỗi (gọi __exit__).

Hai cách: (1) một CLASS có __enter__ (mở, trả thứ gán cho "as") và __exit__ (đóng, chạy kể cả khi lỗi); (2) gọn hơn - @contextlib.contextmanager trên một generator: phần TRƯỚC yield là setup, phần SAU là teardown. Dùng khi bạn cần “mở rồi chắc chắn dọn” (kết nối, khoá, đổi thư mục tạm…).

Encoding quy định cách CHỮ → BYTE. Dùng utf-8 để tiếng Việt không lỗi font: open(path, encoding="utf-8"). Với JSON thêm ensure_ascii=False để giữ chữ Việt đọc được thay vì \uXXXX.

Nếu ghi trực tiếp đè lên file mà chương trình chết giữa chừng (mất điện, crash), file gốc hỏng/nửa chừng. Mẫu an toàn: ghi vào file TẠM, ghi xong mới os.replace(tmp, target) - đổi tên là thao tác NGUYÊN TỬ của OS, nên người đọc luôn thấy bản CŨ hoàn chỉnh hoặc bản MỚI hoàn chỉnh, không bao giờ thấy file dở.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Vì sao nên dùng pathlib thay vì nối chuỗi đường dẫn bằng tay?

  1. 1

    pathlib khám phá

    Dùng Path: liệt kê mọi *.py trong một thư mục (đệ quy), in tên + kích thước (.stat().st_size).

    Hoàn thành khi: Dùng rglob("*.py") + .name + .stat(); ra danh sách file kèm kích thước.

  2. 2

    with & vì sao an toàn

    Dùng widget Bước 2 (kịch bản "Có lỗi"): vì sao file vẫn được đóng dù trong khối with xảy ra lỗi?

    Hoàn thành khi: with luôn gọi __exit__ (đóng file) khi rời khối, kể cả khi có lỗi - rồi lỗi mới lan ra.

  3. 3

    Tự viết context manager

    Viết @contextmanager doi_thu_muc(p): đổi cwd sang p, yield, rồi đổi lại cwd cũ ở teardown.

    Hoàn thành khi: Trong with doi_thu_muc(...), cwd là p; ra khỏi with, cwd trở lại như cũ (kể cả khi lỗi).

  4. 4

    JSON round-trip

    Ghi một dict (tiếng Việt) ra JSON (ensure_ascii=False) rồi đọc lại; kiểm khớp.

    Hoàn thành khi: JSON giữ tiếng Việt đọc được; đọc lại bằng dict gốc.

  5. 5

    Ghi an toàn

    Viết hàm ghi_an_toan(path, text): ghi ra path.tmp rồi os.replace sang path.

    Hoàn thành khi: File đích luôn hoàn chỉnh; nếu lỗi trước os.replace, file cũ không bị hỏng.