Mục lục
- Mục tiêu bài học
- Vì sao cần xử lý lỗi
- Exception trong Python là gì
- Cú pháp try / except cơ bản
- Bắt loại exception cụ thể
- Lấy thông tin exception với as
- Mệnh đề else
- Mệnh đề finally
- raise — chủ động phát exception
- Re-raise trong except
- Custom exception
- Anti-pattern thường gặp
- Exception phổ biến trong AI / data
- Use case: retry network call & validate input
- Bài tập
- Tóm tắt
Mục tiêu bài học
- Hiểu khái niệm exception và cây phân cấp
BaseException → Exception → .... - Viết
try / except / else / finallyđúng vị trí, đúng vai trò. - Bắt exception cụ thể, lấy thông tin lỗi qua
as e. - Dùng
raiseđể phát lỗi, dùngraisetrần để re-raise. - Tạo custom exception cho domain riêng của project.
- Nhận diện anti-pattern (bắt bare
except:, nuốt lỗi, dùng exception cho control flow thường). - Áp dụng vào tình huống AI / data: retry API call, validate input.
Vì sao cần xử lý lỗi
Code AI / data hiếm khi chỉ tính toán trên dữ liệu sạch trong RAM. Thường nó phải:
- Đọc file CSV, JSON, ảnh — file có thể không tồn tại, sai format, encoding lạ.
- Gọi API model (OpenAI, Anthropic, HuggingFace) — mạng có thể timeout, server trả 429, 500.
- Load model checkpoint — file có thể corrupt, version mismatch.
- Parse user input — có thể không phải số, vượt range, rỗng.
Nếu không xử lý lỗi, chương trình sẽ ngừng ngay lần đầu gặp tình huống bất thường. Trong batch job chạy 10 giờ qua 100k record, một KeyError ở record thứ 50k có thể vứt bỏ toàn bộ tiến độ.
Xử lý lỗi tốt cho phép code: bỏ qua record hỏng, retry thao tác có thể thoáng qua (network), báo lỗi rõ ràng cho người dùng thay vì traceback Python thô.
Exception trong Python là gì
Trong Python, mỗi lỗi runtime là một object thuộc một class kế thừa từ BaseException. Khi xảy ra lỗi, interpreter tạo object đó và raise nó lên — luồng thực thi nhảy ra khỏi điểm lỗi, đi ngược stack call cho đến khi gặp khối try bắt được, hoặc thoát chương trình.
Phân cấp rút gọn (Python 3.12):
BaseException
├── SystemExit # sys.exit() raise
├── KeyboardInterrupt # Ctrl+C
├── GeneratorExit
└── Exception # base cho hầu hết lỗi user-level
├── ArithmeticError
│ └── ZeroDivisionError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── ValueError
├── TypeError
├── OSError
│ └── FileNotFoundError
└── ...
Quy ước quan trọng: code ứng dụng chỉ nên bắt class kế thừa Exception, KHÔNG bắt BaseException. Vì BaseException bao gồm cả KeyboardInterrupt và SystemExit — nếu bắt nó, người dùng nhấn Ctrl+C cũng không thoát được.
Cú pháp try / except cơ bản
Khung tối thiểu:
# Đoạn code có thể lỗi đặt trong try
try:
x = int("abc") # ValueError
except Exception:
# Chạy khi try ném ra exception bất kỳ (kế thừa Exception)
print("Không parse được số")
Khi int("abc") raise, dòng đó dừng giữa chừng, control nhảy vào khối except. Code sau try vẫn tiếp tục chạy bình thường.
Lưu ý: KHÔNG dùng bare except:. Cú pháp except: không tham số bắt cả BaseException — gồm cả Ctrl+C và SystemExit. Đây là anti-pattern phổ biến nhất. Tối thiểu hãy dùng except Exception:.
# SAI - bắt mọi thứ kể cả KeyboardInterrupt
try:
risky()
except:
pass
# Đỡ hơn nhưng vẫn quá rộng
try:
risky()
except Exception:
pass
Bắt loại exception cụ thể
Quy tắc thực hành: chỉ bắt loại exception bạn biết cách xử lý. Mọi loại khác hãy để bubble lên — chúng là bug cần fix, không phải tình huống cần che giấu.
def parse_age(s):
try:
return int(s)
except ValueError:
# Chỉ bắt khi không parse được số
return None
Bắt nhiều loại trong một except bằng tuple:
try:
result = process(data)
except (ValueError, TypeError):
# Bắt cả ValueError lẫn TypeError, cùng cách xử lý
result = default_value
Hoặc tách thành nhiều khối except nếu mỗi loại có cách xử lý riêng:
try:
data = load_config("config.yaml")
except FileNotFoundError:
# Tạo config mặc định
data = {"lr": 1e-3, "batch_size": 32}
except ValueError:
# File tồn tại nhưng nội dung sai
print("Config sai format, dừng")
raise
Thứ tự khối except có ý nghĩa: Python kiểm tra từ trên xuống, dừng ở khối khớp đầu tiên. Vì vậy nếu đặt class cha (vd Exception) ở trên, mọi khối phía dưới sẽ không bao giờ chạy.
Lấy thông tin exception với as
Cú pháp except ExceptionType as name gán object exception vào biến name để đọc thông tin:
try:
int("abc")
except ValueError as e:
print(type(e).__name__) # ValueError
print(str(e)) # invalid literal for int() with base 10: 'abc'
print(e.args) # ("invalid literal for int() with base 10: 'abc'",)
Với exception có attribute riêng (ví dụ OSError), có thể đọc thêm:
try:
open("khong-co-file.txt")
except FileNotFoundError as e:
print(e.errno) # 2
print(e.filename) # 'khong-co-file.txt'
Biến e chỉ tồn tại trong khối except. Sau khối, Python xoá nó để giải phóng reference đến traceback. Nếu cần dùng ngoài, hãy gán sang biến khác.
Mệnh đề else
else chạy khi và chỉ khi khối try không raise exception nào. Mục đích: tách phần code có thể lỗi (trong try) khỏi phần code chỉ chạy khi success (trong else).
try:
f = open("data.csv")
except FileNotFoundError:
print("File không có")
else:
# Chỉ chạy khi open() không raise
content = f.read()
f.close()
print(f"Đọc được {len(content)} byte")
Vì sao không gộp content = f.read() vào trong try? Vì nếu gộp, một FileNotFoundError từ chỗ khác (ví dụ file dependency) cũng có thể vô tình rơi vào cùng khối except. else giúp try chỉ giữ đúng dòng có thể raise loại lỗi ta muốn bắt.
Mệnh đề finally
finally luôn chạy, bất kể try có raise hay không, có bị bắt hay không, thậm chí cả khi trong try có return. Dùng để cleanup: đóng file, đóng DB connection, giải phóng GPU memory, ghi log "đã chạy".
f = None
try:
f = open("data.csv")
process(f.read())
except FileNotFoundError:
print("File không có")
finally:
# Chạy dù process() có raise hay không
if f is not None:
f.close()
Trật tự đầy đủ là try → except → else → finally. Cả 4 đều optional trừ try, nhưng phải có ít nhất except hoặc finally đi kèm.
try:
x = compute()
except ValueError:
x = 0
else:
print("compute() ok")
finally:
print("dọn dẹp")
Trong code thực tế, với file và connection nên dùng with statement (context manager) — sẽ học ở bài 17. Pattern try / finally + close chỉ cần khi không có context manager phù hợp.
raise — chủ động phát exception
Khi hàm phát hiện input không hợp lệ, hoặc state không nên xảy ra, hãy raise exception thay vì trả về giá trị "sentinel" như -1 hay None.
def softmax_temperature(logits, T):
if T <= 0:
# Báo lỗi rõ ràng cho caller
raise ValueError(f"T phải > 0, nhận được {T}")
# ... tính toán
Quy ước chọn class:
ValueError— đúng kiểu, sai giá trị (vd số âm cho cái cần dương).TypeError— sai kiểu (vd truyền string khi cần int).KeyError— key không có trong dict.IndexError— index ngoài range của list / tuple.NotImplementedError— method abstract chưa override.RuntimeError— state không hợp lệ, không khớp loại nào ở trên.
Tránh raise Exception trần — caller sẽ phải bắt rộng và mất ý nghĩa.
Re-raise trong except
Đôi khi muốn làm gì đó khi bắt được exception (log, cleanup, increment counter) nhưng vẫn để lỗi bubble lên trên. Dùng raise trần (không đối số) bên trong except:
def load_model(path):
try:
model = torch.load(path)
except FileNotFoundError:
logger.error(f"Không tìm thấy checkpoint: {path}")
raise # Re-raise nguyên exception gốc, giữ traceback
return model
Còn nếu muốn thay bằng exception khác nhưng giữ context, dùng raise NewException(...) from e:
def load_config(path):
try:
with open(path) as f:
return json.load(f)
except FileNotFoundError as e:
# Chuyển sang exception domain-specific
raise ConfigError(f"Config '{path}' không có") from e
Khi from e được dùng, traceback hiển thị cả lỗi gốc lẫn lỗi mới — debug dễ hơn vì biết nguyên nhân chuỗi.
Custom exception
Project lớn nên định nghĩa exception riêng cho domain. Giúp caller phân biệt lỗi của thư viện với lỗi của ứng dụng.
# exceptions.py
class AppError(Exception):
"""Base cho mọi exception của app."""
class ConfigError(AppError):
"""Lỗi liên quan tới file config."""
class ModelLoadError(AppError):
"""Lỗi khi load model checkpoint."""
class APIRateLimitError(AppError):
"""API trả 429."""
Trong code chính:
try:
run_pipeline()
except ConfigError as e:
# Lỗi user-fixable, in hướng dẫn
print(f"Cấu hình sai: {e}")
except ModelLoadError as e:
print(f"Không load được model: {e}")
except AppError as e:
# Bắt mọi lỗi domain còn lại
print(f"App error: {e}")
Custom exception kế thừa Exception, không phải BaseException. Thường chỉ cần một dòng pass hoặc docstring trong body — Python lo phần còn lại.
Anti-pattern thường gặp
1. Bắt quá rộng rồi nuốt lỗi.
# SAI - mọi bug đều biến mất, không log
try:
big_function()
except Exception:
pass
Lỗi đáng lẽ phải vỡ ngay (vd typo tên biến → NameError) bị nuốt mất, debug rất khó.
2. Dùng exception cho control flow thông thường.
# SAI - dùng exception để check key
try:
v = d["foo"]
except KeyError:
v = None
# ĐÚNG - check trực tiếp
v = d.get("foo")
Lưu ý: pattern "EAFP" (Easier to Ask Forgiveness than Permission) của Python chấp nhận dùng try/except trong vài tình huống — nhưng đừng dùng cho thao tác có cách check rẻ tiền (dict.get, "x" in d, hasattr).
3. Log rồi raise — lặp ý.
# DƯ - traceback đã chứa thông tin
try:
risky()
except Exception as e:
logger.error(f"Có lỗi: {e}")
raise
Nếu chỉ log và re-raise, Python sẽ log lỗi 2 lần (một lần từ logger.error, một lần khi exception bubble lên unhandled handler). Hoặc chỉ log + không raise (mất bubble), hoặc dùng logger.exception(...) ở chỗ cuối cùng bắt được.
4. except Exception trên đoạn code dài. Càng nhiều dòng trong try, càng nhiều khả năng bắt nhầm lỗi không liên quan. Giữ try chỉ chứa chính dòng có thể raise.
Exception phổ biến trong AI / data
FileNotFoundError— file dataset, checkpoint, config không tồn tại.PermissionError— không có quyền đọc / ghi (vd ghi vào/usr/local).UnicodeDecodeError— đọc file CSV với encoding sai.json.JSONDecodeError— parse JSON malformed (response API hỏng).KeyError— truy cập dict / row pandas thiếu cột.IndexError— index list / tensor ngoài range.ValueError— convert sai (int("abc"),np.arrayvới shape không khớp).TypeError— gọi hàm sai kiểu (truyền list vào chỗ cần ndarray).ZeroDivisionError— chia 0 (vd normalize với std = 0).StopIteration— iterator hết phần tử (thường được Python xử lý ngầm).requests.HTTPError/requests.Timeout— gọi API model thất bại.torch.cuda.OutOfMemoryError— GPU hết VRAM khi train.
Mỗi loại có cách xử lý khác nhau: FileNotFoundError có thể tạo default; Timeout nên retry; OutOfMemoryError cần giảm batch size.
Use case: retry network call & validate input
14.1. Retry network call. Kết hợp try / except với vòng while (đã học ở bài 9):
import time
import requests
def call_api_with_retry(url, max_retries=3, backoff=2.0):
"""Gọi API, retry tối đa max_retries lần với exponential backoff."""
for attempt in range(max_retries):
try:
r = requests.get(url, timeout=10)
r.raise_for_status() # raise HTTPError nếu status >= 400
return r.json()
except (requests.Timeout, requests.ConnectionError) as e:
# Lỗi network thoáng qua → retry
wait = backoff ** attempt
print(f"Lần {attempt + 1} lỗi ({e}), chờ {wait}s rồi thử lại")
time.sleep(wait)
except requests.HTTPError as e:
# Lỗi từ server: 4xx do mình sai, 5xx có thể retry
if 500 <= e.response.status_code < 600 and attempt < max_retries - 1:
time.sleep(backoff ** attempt)
continue
raise # 4xx hoặc hết lượt retry → để bubble lên
raise RuntimeError(f"Gọi {url} thất bại sau {max_retries} lần")
Lưu ý: chỉ retry với lỗi có thể tự khỏi (timeout, connection, 5xx). Lỗi 401, 404, 422 retry vô ích — phải fix code / config.
14.2. Validate input. Wrap input() trong vòng lặp tới khi user nhập đúng:
def ask_int(prompt, min_value=None, max_value=None):
"""Hỏi user một số nguyên hợp lệ, retry nếu sai."""
while True:
raw = input(prompt)
try:
value = int(raw)
except ValueError:
print("Phải là số nguyên, thử lại")
continue
if min_value is not None and value < min_value:
print(f"Phải >= {min_value}")
continue
if max_value is not None and value > max_value:
print(f"Phải <= {max_value}")
continue
return value
# Sử dụng
age = ask_int("Tuổi: ", min_value=0, max_value=150)
Bài tập
Bài 1: Hàm chia an toàn.
Viết hàm safe_divide(a, b):
- Nếu
ahoặcbkhông phải số (int/float) → raiseTypeErrorvới message rõ ràng. - Nếu
b == 0→ bắtZeroDivisionErrorvà trả vềfloat('inf')với cảnh báo print. - Bình thường trả về
a / b.
Bài 2: Parse CSV bỏ qua dòng lỗi.
Cho list dòng CSV (string), mỗi dòng định dạng "name,age". Viết hàm trả về list dict [{"name": ..., "age": ...}, ...]. Dòng nào có age không phải số nguyên thì bỏ qua, đếm số dòng bị bỏ và in cuối hàm.
rows = [
"alice,30",
"bob,abc", # lỗi
"carol,28",
"dan,", # lỗi
"eve,40",
]
Bài 3: Custom exception.
Định nghĩa InvalidEmailError(Exception). Viết hàm validate_email(s): nếu s không chứa ký tự @ hoặc không có . sau @, raise InvalidEmailError với message giải thích. Gọi hàm trong try / except với 3 input mẫu, in lỗi nếu có.
Bài 4: Retry logic.
Mô phỏng API hay fail bằng hàm flaky_call():
import random
def flaky_call():
if random.random() < 0.7:
raise ConnectionError("Tạm thời lỗi")
return "ok"
Viết wrapper retry(fn, max_attempts=5) dùng try / except ConnectionError để retry. Sau khi hết lượt vẫn lỗi → raise RuntimeError. Test 10 lần và in tỉ lệ thành công.
Bài 5 (mở rộng): cleanup với finally.
Mô phỏng "session": viết hàm open_session() trả về dict {"id": 123, "open": True}, và close_session(s) đặt s["open"] = False. Trong hàm do_work(), mở session, gọi một hàm có thể raise ValueError, dùng finally để đảm bảo session luôn được đóng dù có lỗi hay không.
Tóm tắt
- Exception là object kế thừa
BaseException; code ứng dụng chỉ nên bắt lớp con củaException. - Cú pháp đầy đủ:
try → except → else → finally.elsechạy khi không lỗi,finallyluôn chạy. - Bắt loại cụ thể (
ValueError,FileNotFoundError...), tránh bareexcept:vàexcept Exception:trên đoạn dài. except ... as eđể đọc message,e.args, các attribute riêng (e.errno,e.filename...).raise ValueError("...")để chủ động báo lỗi;raisetrần để re-raise giữ traceback;raise X from eđể chuyển sang exception khác kèm context.- Custom exception kế thừa
Exception, giúp tách lỗi domain với lỗi thư viện. - Anti-pattern: nuốt lỗi, dùng exception thay cho check thông thường, log + raise lặp ý,
trybao quá nhiều dòng. - Use case AI / data: retry network call có backoff cho lỗi thoáng qua; validate input qua loop + try/except.
Bài tiếp theo học open() và context manager — cách đọc / ghi file text đúng chuẩn, không cần thủ công try / finally + close().
