Python 6 — Xử lý lỗi, File I/O & Logging

13 thg 7, 2026 4 lượt xem
#python
#exception
#logging
#file-io

Python 6 — Xử lý lỗi, File I/O & Logging

Một chương trình thực tế không bao giờ chạy trong điều kiện lý tưởng: file biến mất, dữ liệu sai định dạng, mạng rớt, người dùng nhập bậy. Sự khác biệt giữa một script "đồ chơi" và một ứng dụng đáng tin cậy nằm ở chỗ: nó xử lý lỗi ra sao, nó quản lý tài nguyên (file, kết nối) thế nào, và nó để lại dấu vết (log) gì để bạn gỡ lỗi sau này. Bài này đi sâu vào ba trụ cột đó.


Exception là gì?

Khi Python gặp tình huống nó không thể tiếp tục một cách bình thường, nó không trả về mã lỗi như C. Thay vào đó nó raise (ném) một exception — một đối tượng mô tả lỗi. Exception này "nổi lên" (propagate) qua các lời gọi hàm cho đến khi có ai đó bắt nó. Nếu không ai bắt, chương trình dừng và in ra traceback.

def chia(a, b):
    return a / b

chia(10, 0)
# ZeroDivisionError: division by zero

Exception là đối tượng thuộc một cây phân cấp lớp (class hierarchy). Gốc của hầu hết lỗi là Exception. Một số nhánh quan trọng:

  • ValueError — giá trị đúng kiểu nhưng sai nội dung: int("abc").
  • TypeError — sai kiểu: "a" + 1.
  • KeyError / IndexError — truy cập key/index không tồn tại.
  • FileNotFoundError — mở file không có (con của OSError).
  • ZeroDivisionError, AttributeError, StopIteration...

Lưu ý: KeyboardInterruptSystemExit không là con của Exception mà của BaseException. Vì vậy except Exception sẽ không nuốt mất Ctrl+C — đó là chủ ý của Python.


try / except / else / finally

Cấu trúc đầy đủ để xử lý exception gồm bốn khối:

try:
    f = open("data.txt")
    so = int(f.readline())
except FileNotFoundError:
    print("Không tìm thấy file")
except ValueError:
    print("Dòng đầu không phải số")
else:
    print(f"Đọc thành công: {so}")
finally:
    print("Khối này luôn chạy")

Ý nghĩa từng khối:

  • try — đặt đoạn code "có thể hỏng".
  • except — bắt một loại lỗi cụ thể. Có thể có nhiều except; Python kiểm tra lần lượt từ trên xuống, dừng ở khối đầu khớp.
  • else — chạy chỉ khi khối try không ném lỗi. Dùng để tách phần "logic chính" khỏi phần "có thể lỗi".
  • finallyluôn chạy, dù có lỗi hay không, dù có return giữa chừng. Đây là nơi dọn dẹp tài nguyên.

Bắt nhiều loại lỗi cùng lúc

try:
    xu_ly()
except (ValueError, TypeError) as e:
    print(f"Lỗi dữ liệu: {e}")

Biến e là chính đối tượng exception. str(e) cho thông điệp, type(e).__name__ cho tên lớp.

Thứ tự except: cụ thể trước, tổng quát sau

Vì Python dừng ở khối khớp đầu tiên, bạn phải đặt lớp con trước lớp cha:

try:
    mo_file()
except FileNotFoundError:      # con của OSError — phải đứng trước
    print("Thiếu file")
except OSError:                # tổng quát hơn
    print("Lỗi hệ thống file")

Nếu đảo ngược, except OSError sẽ "nuốt" luôn FileNotFoundError.


raise và custom exception

Bạn có thể tự ném lỗi bằng raise:

def rut_tien(so_du, so_tien):
    if so_tien <= 0:
        raise ValueError("Số tiền phải dương")
    if so_tien > so_du:
        raise ValueError(f"Không đủ tiền: cần {so_tien}, có {so_du}")
    return so_du - so_tien

Tự định nghĩa exception

Khi lỗi mang ý nghĩa nghiệp vụ riêng, hãy tạo lớp exception riêng bằng cách kế thừa Exception:

class SoDuKhongDuError(Exception):
    """Tài khoản không đủ số dư."""
    def __init__(self, so_du, so_tien):
        self.so_du = so_du
        self.so_tien = so_tien
        super().__init__(f"Cần {so_tien}, chỉ có {so_du}")

def rut_tien(so_du, so_tien):
    if so_tien > so_du:
        raise SoDuKhongDuError(so_du, so_tien)
    return so_du - so_tien

try:
    rut_tien(100, 500)
except SoDuKhongDuError as e:
    print(e)              # Cần 500, chỉ có 100
    print(e.so_tien)      # truy cập dữ liệu kèm theo

Custom exception giúp người gọi bắt đúng loại lỗi của bạn, không lẫn với lỗi khác.

raise ... from: giữ chuỗi nguyên nhân

Khi bắt một lỗi rồi ném lỗi khác, dùng from để giữ ngữ cảnh gốc:

try:
    cau_hinh = json.loads(noi_dung)
except json.JSONDecodeError as e:
    raise ValueError("File cấu hình hỏng") from e

Traceback sẽ hiển thị cả hai: lỗi JSON gốc và lỗi cấu hình bạn ném ra — rất hữu ích khi gỡ lỗi.


EAFP vs LBYL

Đây là hai triết lý xử lý tình huống bất thường.

  • LBYLLook Before You Leap: kiểm tra điều kiện trước khi hành động.
  • EAFPEasier to Ask Forgiveness than Permission: cứ làm, nếu lỗi thì bắt.
# LBYL
if "ten" in du_lieu:
    ten = du_lieu["ten"]
else:
    ten = "Khách"

# EAFP — phong cách Python ưa chuộng
try:
    ten = du_lieu["ten"]
except KeyError:
    ten = "Khách"

Python nghiêng về EAFP. Lý do: với LBYL, giữa lúc kiểm tra và lúc hành động dữ liệu có thể thay đổi (race condition), và bạn thường phải kiểm tra nhiều điều kiện lồng nhau. EAFP gọn hơn và an toàn hơn trong môi trường đa luồng. (Với trường hợp dict đơn giản, du_lieu.get("ten", "Khách") còn ngắn hơn cả hai.)


Context manager và câu lệnh with

Vấn đề kinh điển: bạn mở một file, xử lý, rồi quên đóng — hoặc một exception ném ra trước khi kịp gọi close(). File bị giữ, dữ liệu chưa flush xuống đĩa.

# Cách dở: nếu xu_ly() ném lỗi, f.close() không bao giờ chạy
f = open("data.txt")
xu_ly(f)
f.close()

Câu lệnh with giải quyết triệt để: nó đảm bảo tài nguyên được dọn dẹp tự động, kể cả khi có exception.

with open("data.txt") as f:
    xu_ly(f)
# Đến đây file CHẮC CHẮN đã đóng, dù xu_ly() có lỗi hay không

Cơ chế: open() trả về một context manager — đối tượng có hai phương thức __enter__ (chạy khi vào khối with, trả về giá trị gán cho f) và __exit__ (chạy khi rời khối, kể cả do exception). Đây thực chất là try/finally được gói gọn lại.

Tự viết context manager

Cách đơn giản nhất là dùng decorator contextmanager:

from contextlib import contextmanager
import time

@contextmanager
def do_thoi_gian(ten):
    bat_dau = time.perf_counter()
    try:
        yield                      # code trong khối with chạy ở đây
    finally:
        thoi_gian = time.perf_counter() - bat_dau
        print(f"{ten} mất {thoi_gian:.3f}s")

with do_thoi_gian("xử lý dữ liệu"):
    sum(range(10_000_000))

Phần trước yield__enter__, phần sau (trong finally) là __exit__.

Bạn cũng có thể mở nhiều tài nguyên trong một with:

with open("nguon.txt") as src, open("dich.txt", "w") as dst:
    dst.write(src.read())

Đọc/ghi file văn bản

Hàm open(path, mode, encoding=...) là cổng vào file. Các chế độ (mode) chính:

ModeÝ nghĩa
"r"Đọc (mặc định), lỗi nếu file không tồn tại
"w"Ghi, xóa nội dung cũ
"a"Ghi nối thêm vào cuối
"x"Tạo mới, lỗi nếu đã tồn tại
"b"Chế độ nhị phân (kết hợp: "rb", "wb")

Luôn chỉ định encoding="utf-8" với file văn bản để tránh lỗi tùy thuộc hệ điều hành:

# Ghi
with open("ghi_chu.txt", "w", encoding="utf-8") as f:
    f.write("Dòng 1\n")
    f.writelines(["Dòng 2\n", "Dòng 3\n"])

# Đọc toàn bộ
with open("ghi_chu.txt", encoding="utf-8") as f:
    noi_dung = f.read()

# Đọc từng dòng — tiết kiệm bộ nhớ với file lớn
with open("ghi_chu.txt", encoding="utf-8") as f:
    for dong in f:                  # file là iterable theo dòng
        print(dong.rstrip())        # bỏ ký tự xuống dòng cuối

Đọc từng dòng bằng vòng lặp for là cách streaming: chỉ một dòng nằm trong bộ nhớ mỗi lần, nên xử lý được file hàng GB mà không tràn RAM.

File nhị phân

Với ảnh, file nén, dữ liệu nhị phân — dùng mode "b", làm việc với bytes thay vì str:

with open("anh.png", "rb") as f:
    du_lieu = f.read()        # kiểu bytes
print(len(du_lieu), "byte")
print(du_lieu[:8])            # 8 byte đầu (chữ ký file PNG)

Làm việc với JSON

JSON là định dạng trao đổi dữ liệu phổ biến nhất (API, file cấu hình). Module json có sẵn trong Python:

import json

du_lieu = {
    "ten": "Lan",
    "tuoi": 30,
    "ky_nang": ["python", "sql"],
    "active": True,
}

# Đối tượng Python -> chuỗi JSON
chuoi = json.dumps(du_lieu, ensure_ascii=False, indent=2)
print(chuoi)

# Ghi thẳng ra file
with open("user.json", "w", encoding="utf-8") as f:
    json.dump(du_lieu, f, ensure_ascii=False, indent=2)

# Đọc từ file -> đối tượng Python
with open("user.json", encoding="utf-8") as f:
    lai = json.load(f)
print(lai["ten"], type(lai))   # Lan <class 'dict'>

Ghi nhớ bốn hàm:

  • json.dumps / json.loads — làm việc với chuỗi (s = string).
  • json.dump / json.load — làm việc với file.

Tham số ensure_ascii=False giữ tiếng Việt nguyên bản (nếu để mặc định, dấu tiếng Việt sẽ bị escape thành \uXXXX). indent=2 cho output dễ đọc.

Khi JSON sai cú pháp, bắt json.JSONDecodeError:

try:
    cau_hinh = json.loads(van_ban)
except json.JSONDecodeError as e:
    print(f"JSON hỏng ở dòng {e.lineno}: {e.msg}")

Làm việc với CSV

CSV là định dạng bảng (Excel, dữ liệu xuất từ DB). Module csv xử lý đúng các trường hợp khó như dấu phẩy bên trong ô, dấu nháy, xuống dòng:

import csv

# Ghi
hang = [
    ["ten", "tuoi", "thanh_pho"],
    ["Lan", 30, "Hà Nội"],
    ["Minh", 25, "Đà Nẵng"],
]
with open("nguoi.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerows(hang)

# Đọc thành list các dòng
with open("nguoi.csv", newline="", encoding="utf-8") as f:
    for dong in csv.reader(f):
        print(dong)            # ['Lan', '30', 'Hà Nội'] — lưu ý: toàn str

Lưu ý quan trọng: luôn truyền newline="" khi mở file CSV để module tự quản lý ký tự xuống dòng (tránh dòng trống thừa trên Windows).

DictReader / DictWriter tiện hơn khi làm việc theo tên cột:

with open("nguoi.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        print(row["ten"], row["thanh_pho"])   # truy cập theo cột

with open("ra.csv", "w", newline="", encoding="utf-8") as f:
    cols = ["ten", "tuoi"]
    writer = csv.DictWriter(f, fieldnames=cols)
    writer.writeheader()
    writer.writerow({"ten": "Hoa", "tuoi": 28})

pathlib — thao tác đường dẫn hiện đại

Thay vì nối chuỗi đường dẫn (dễ sai dấu / \ giữa các hệ điều hành), dùng pathlib.Path hướng đối tượng:

from pathlib import Path

thu_muc = Path("du_lieu")
file = thu_muc / "bao_cao.txt"        # toán tử / để nối — đa nền tảng

print(file.name)        # bao_cao.txt
print(file.suffix)      # .txt
print(file.parent)      # du_lieu
print(file.exists())    # True/False

# Tạo thư mục (cả cây cha) nếu chưa có
thu_muc.mkdir(parents=True, exist_ok=True)

# Đọc/ghi nhanh không cần open()
file.write_text("xin chào", encoding="utf-8")
print(file.read_text(encoding="utf-8"))

# Duyệt tất cả file .csv trong thư mục
for f in thu_muc.glob("*.csv"):
    print(f)

pathlib gọn, an toàn đa nền tảng và là cách được khuyến nghị thay cho module os.path cũ.


Logging — vì sao tốt hơn print

print() để gỡ lỗi có nhiều nhược điểm: không phân biệt mức độ nghiêm trọng, không có timestamp, không tắt được khi lên production, không gửi được tới file hay hệ thống tập trung. Module logging giải quyết tất cả.

Các level

Logging có năm cấp độ, từ thấp đến cao: DEBUG < INFO < WARNING < ERROR < CRITICAL. Bạn đặt một ngưỡng; chỉ thông điệp bằng hoặc cao hơn ngưỡng mới được ghi.

import logging

logging.basicConfig(
    level=logging.INFO,                          # ngưỡng: bỏ qua DEBUG
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
log = logging.getLogger(__name__)

log.debug("Chi tiết gỡ lỗi")     # KHÔNG hiện (dưới ngưỡng)
log.info("Bắt đầu xử lý")        # hiện
log.warning("File thiếu cột B")  # hiện
log.error("Không kết nối được DB")

Ưu điểm thực dụng: trên dev đặt level=DEBUG để thấy mọi thứ; lên production đổi sang INFO hoặc WARNINGkhông phải sửa code, chỉ đổi một dòng cấu hình.

Logger, Handler, Formatter

Ba thành phần cốt lõi:

  • Logger — đối tượng bạn gọi (log.info(...)). Nên đặt tên theo module: logging.getLogger(__name__).
  • Handler — quyết định log đi đâu: ra console (StreamHandler), ra file (FileHandler), gửi mạng... Một logger có thể có nhiều handler.
  • Formatter — quyết định log trông như thế nào (timestamp, level, message).
import logging

log = logging.getLogger("xu_ly_du_lieu")
log.setLevel(logging.DEBUG)

# Handler 1: ra file, ghi mọi thứ từ DEBUG
fh = logging.FileHandler("app.log", encoding="utf-8")
fh.setLevel(logging.DEBUG)

# Handler 2: ra console, chỉ WARNING trở lên
ch = logging.StreamHandler()
ch.setLevel(logging.WARNING)

fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
fh.setFormatter(fmt)
ch.setFormatter(fmt)

log.addHandler(fh)
log.addHandler(ch)

log.debug("Chi tiết -> chỉ vào file")
log.error("Lỗi nghiêm trọng -> cả file lẫn console")

Ghi log exception kèm traceback

Trong khối except, dùng log.exception() (hoặc exc_info=True) để ghi cả traceback:

try:
    rui_ro()
except Exception:
    log.exception("Xử lý thất bại")   # tự động kèm traceback đầy đủ

Sơ đồ luồng xử lý file an toàn

Điểm mấu chốt của sơ đồ: dù đi nhánh lỗi hay nhánh thành công, __exit__ của with luôn đóng file trước khi chương trình tiếp tục.


Ví dụ thực tế: xử lý dữ liệu đầu-cuối

Đọc một file CSV log giao dịch, lọc giao dịch lỗi, xuất bản tóm tắt ra JSON, ghi log toàn quá trình:

import csv
import json
import logging
from pathlib import Path

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger(__name__)


class DuLieuLoiError(Exception):
    """Dòng dữ liệu không hợp lệ."""


def doc_giao_dich(path: Path):
    tong = 0.0
    so_dong_loi = 0
    with open(path, newline="", encoding="utf-8") as f:
        for i, row in enumerate(csv.DictReader(f), start=2):  # dòng 1 là header
            try:
                so_tien = float(row["so_tien"])
                if so_tien < 0:
                    raise DuLieuLoiError("số tiền âm")
                tong += so_tien
            except (ValueError, KeyError, DuLieuLoiError) as e:
                so_dong_loi += 1
                log.warning("Bỏ qua dòng %d: %s", i, e)
    return tong, so_dong_loi


def main():
    nguon = Path("giao_dich.csv")
    try:
        tong, loi = doc_giao_dich(nguon)
    except FileNotFoundError:
        log.error("Không tìm thấy file: %s", nguon)
        return
    except Exception:
        log.exception("Lỗi không lường trước")
        return

    ket_qua = {"tong_tien": tong, "so_dong_loi": loi}
    Path("tom_tat.json").write_text(
        json.dumps(ket_qua, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    log.info("Hoàn tất: tổng=%.2f, lỗi=%d", tong, loi)


if __name__ == "__main__":
    main()

Ví dụ này gộp đủ mọi ý của bài: with đảm bảo đóng file; try/except bắt nhiều loại lỗi cụ thể; custom exception cho lỗi nghiệp vụ; csv.DictReader đọc theo cột; pathlib cho đường dẫn; json xuất kết quả; và logging để lại dấu vết thay vì print. Một dòng dữ liệu hỏng không làm sập cả tiến trình — nó được log lại và bỏ qua, đúng tinh thần "fail gracefully".


Tóm tắt

  • Exception là đối tượng mô tả lỗi, propagate cho đến khi được bắt; dùng try/except/else/finally, đặt lỗi cụ thể trước lỗi tổng quát.
  • raise để ném lỗi; tạo custom exception kế thừa Exception cho lỗi nghiệp vụ; raise ... from giữ chuỗi nguyên nhân.
  • Python ưa EAFP (cứ làm rồi bắt lỗi) hơn LBYL (kiểm tra trước).
  • Câu lệnh with (context manager) đảm bảo tài nguyên được dọn dẹp kể cả khi có exception — luôn dùng nó khi mở file.
  • Đọc file theo dòng để streaming tiết kiệm bộ nhớ; luôn đặt encoding="utf-8".
  • Module json (dump/load/dumps/loads) và csv (reader/DictReader) xử lý hai định dạng dữ liệu phổ biến nhất; pathlib.Path cho thao tác đường dẫn đa nền tảng.
  • logging vượt trội print: có level, handler, formatter, timestamp và tắt/bật theo môi trường mà không sửa code.

Tự kiểm tra

  1. Sự khác nhau giữa khối elsefinally trong try là gì? Khi nào mỗi khối được chạy?
  2. Vì sao phải đặt except FileNotFoundError trước except OSError? Điều gì xảy ra nếu đảo ngược?
  3. Giải thích cơ chế __enter__/__exit__ khiến câu lệnh with đóng file an toàn ngay cả khi xảy ra exception.
  4. Phân biệt json.dump với json.dumps, và json.load với json.loads.
  5. Vì sao logging tốt hơn print() khi triển khai ứng dụng lên production? Nêu ít nhất hai lý do.
  6. Khi nào nên định nghĩa một custom exception thay vì dùng ValueError có sẵn?

Đọc tiếp

Python 7 — Pandas & NumPy

Bài viết liên quan

Kết nối CSDL với SQLAlchemy, gọi REST API với requests/httpx, và lập trình bất đồng bộ asyncio.

13 thg 7, 2026 4

Định nghĩa hàm, tham số, *args/**kwargs, lambda, module/package, pip và virtualenv.

13 thg 7, 2026 4

Lớp, kế thừa, đa hình, dunder methods, dataclass, type hints và nguyên tắc viết code sạch.

13 thg 7, 2026 4

Vì sao Python là ngôn ngữ số một của data engineer: vai trò trong pipeline (ingest/transform/orchestrate), hệ sinh thái thư viện (pandas/polars/pyarrow/sqlalchemy), quản lý môi trường (venv/uv/poetry), và khi nào dùng Python vs SQL/Spark.

13 thg 7, 2026 4