Python 6 — Xử lý lỗi, File I/O & Logging
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ủaOSError).ZeroDivisionError,AttributeError,StopIteration...
Lưu ý:
KeyboardInterruptvàSystemExitkhông là con củaExceptionmà củaBaseException. Vì vậyexcept Exceptionsẽ 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ềuexcept; 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ốitrykhông ném lỗi. Dùng để tách phần "logic chính" khỏi phần "có thể lỗi".finally— luôn chạy, dù có lỗi hay không, dù córeturngiữ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.
- LBYL — Look Before You Leap: kiểm tra điều kiện trước khi hành động.
- EAFP — Easier 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 là __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 WARNING — khô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ừaExceptioncho lỗi nghiệp vụ;raise ... fromgiữ 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.Pathcho thao tác đường dẫn đa nền tảng. loggingvượt trộiprint: 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
- Sự khác nhau giữa khối
elsevàfinallytrongtrylà gì? Khi nào mỗi khối được chạy? - Vì sao phải đặt
except FileNotFoundErrortrướcexcept OSError? Điều gì xảy ra nếu đảo ngược? - Giải thích cơ chế
__enter__/__exit__khiến câu lệnhwithđóng file an toàn ngay cả khi xảy ra exception. - Phân biệt
json.dumpvớijson.dumps, vàjson.loadvớijson.loads. - Vì sao
loggingtốt hơnprint()khi triển khai ứng dụng lên production? Nêu ít nhất hai lý do. - Khi nào nên định nghĩa một custom exception thay vì dùng
ValueErrorcó sẵn?
Đọc tiếp
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.
Định nghĩa hàm, tham số, *args/**kwargs, lambda, module/package, pip và virtualenv.
Lớp, kế thừa, đa hình, dunder methods, dataclass, type hints và nguyên tắc viết code sạch.
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.