Khi Nào Nên Dùng Dataclass, Generator và Try/Except Trong Python
Phần 2 của bộ hướng dẫn tư duy thiết kế (mental checklist) cho lập trình viên Python. Biết cách lựa chọn giữa Dict và Dataclass, Lazy vs Eager, và triết lý xử lý lỗi.
Bạn đã nắm vững các kiến thức cơ bản về class và function, nhưng Python thường cung cấp nhiều cách để giải quyết cùng một vấn đề. Tiến độ code của bạn sẽ chững lại khi bạn bắt đầu đắn đo suy nghĩ:
- Mình nên truyền dữ liệu này dưới dạng
dict(từ điển) haydataclass? - Chỗ này dùng List Comprehension (tạo list) hay Generator (trình phát sinh lazy)?
- Mình nên kiểm tra key có tồn tại hay không bằng lệnh
if, hay cứ dùngtry/except? - Hàm này nên được gắn
@staticmethod, hay chỉ là một hàm độc lập viết bên ngoài class? - Mình nên viết rõ cấu trúc từng tham số, hay cứ dùng gọn
**kwargs?
Bài viết này là Phần 2 của bộ hướng dẫn tư duy (checklist). Dưới đây là 5 quyết định thiết kế Python thường gặp mỗi ngày với các ví dụ cụ thể.
1) Dictionary vs dataclass
Dùng dict khi:
- Các keys (khóa) phụ thuộc vào thời điểm chạy (runtime) (ví dụ: gộp dữ liệu theo User ID động).
- Dữ liệu đơn giản, không có cấu trúc cố định và được luân chuyển nhanh trong hệ thống.
- Bạn cần serialize (chuyển đổi) xuống JSON ngay lập tức.
Dùng dataclass khi:
- Bạn biết chính xác các trường (fields) từ trước (schema cố định).
- Bạn muốn tránh lỗi gõ sai chữ (được IDE gợi ý và kiểm tra kiểu với
mypy). - Bạn dự định gắn các hành vi (methods) vào cục dữ liệu đó sau này.
Ví dụ: Cái bẫy “gõ sai” với dicts
# Khi dùng dict, gõ sai chữ sẽ tạo ra lỗi runtime (hoặc bug ngầm)
user = {"first_name": "Alice", "role": "admin"}
print(user.get("firstname")) # Trả về None. Lỗi ngầm!
Ví dụ: Dataclass mang lại sự an toàn
from dataclasses import dataclass
@dataclass
class User:
first_name: str
role: str
user = User(first_name="Alice", role="admin")
print(user.first_name) # IDE sẽ tự động gợi ý. Gõ sai? mypy sẽ báo lỗi trước cả khi chạy.
2) List Comprehension (Eager) vs Generator (Lazy)
Dùng List Comprehension [...] khi:
- Bạn cần biết số lượng kết quả ngay (
len()). - Bạn cần lặp qua danh sách dữ liệu đó nhiều lần.
- Tập dữ liệu đủ nhỏ để có thể nằm gọn trong RAM.
- Bạn cần sắp xếp (sort) hoặc cắt lát (slice) dữ liệu từ cuối lên.
Dùng Generator Expression (...) hoặc yield khi:
- Bạn phải xử lý tập dữ liệu khổng lồ (logs, file lớn, kết quả từ database).
- Bạn muốn nối chuỗi (chaining) nhiều bước xử lý với nhau (pipelines).
- Bạn có thể dừng vòng lặp sớm (ví dụ: bẻ khóa vòng lặp ngay khi tìm thấy kết quả đầu tiên).
Ví dụ: Đừng tải toàn bộ file vào bộ nhớ
# TỒI: Đọc toàn bộ file log 5GB vào RAM dưới dạng một List
def get_error_logs(file_path: str) -> list[str]:
with open(file_path) as f:
return [line for line in f if "ERROR" in line]
# TỐT: Lấy từng dòng một bằng Yield (Lazy evaluation - Trì hoãn thực thi)
def stream_error_logs(file_path: str):
with open(file_path) as f:
for line in f:
if "ERROR" in line:
yield line
# Bắt lỗi ĐẦU TIÊN diễn ra ngay lập tức và tốn 0 MB RAM!
first_error = next(stream_error_logs("app.log"))
3) try/except (EAFP) vs if/else (LBYL)
Python luôn đề cao triết lý EAFP: “Dễ dàng xin tha thứ hơn là xin phép” (Easier to Ask for Forgiveness than Permission). Trong khi hầu hết các ngôn ngữ khác thích LBYL: “Nhìn kỹ trước khi nhảy” (Look Before You Leap).
Dùng if/else (LBYL) khi:
- Trường hợp bị lỗi/hỏng xảy ra thường xuyên (ví dụ: chiếm 30%). Catch Exception sẽ rất chậm đổi với các lỗi diễn ra liên tục.
- Việc kiểm tra điều kiện (check) tiêu tốn ít chi phí tính toán.
Dùng try/except (EAFP) khi:
- “Con đường hạnh phúc” (Happy path) chiếm tới 99% thời gian chạy.
- Bạn muốn tránh Race Conditions (ví dụ: một file bị luồng khác xóa mất sau khi bạn đã dùng if kiểm tra nó tồn tại, nhưng xảy ra trước khi bạn kịp mở nó).
- Code nhìn gọn gàng và phẳng hơn, không bị lồng quá nhiều cấp độ
if/else.
Ví dụ: Tránh Race Conditions với EAFP
import os
# LBYL (Look before you leap) - Rất dễ dính Race Condition!
if os.path.exists("config.json"):
# Điều gì xảy ra nếu tại ĐÂY một process khác xóa mất file? Chương trình văng lỗi ngay!
with open("config.json") as f:
config = f.read()
# EAFP (Easier to ask for forgiveness) - Cực kì chuẩn Python!
try:
with open("config.json") as f:
config = f.read()
except FileNotFoundError:
config = "{}"
4) @staticmethod vs Hàm cấp Module (Module-Level Function)
Dùng module-level function khi:
- Hàm không cần tới
self(biến thực thể) haycls(biến lớp). Trong Python, bạn không cần phải nhồi nhét mọi thứ vào trong class giống như Java hay C#.
Dùng @staticmethod khi (Rất hiếm):
- Về mặt logic, hàm chắc chắn và khắt khe thuộc về không gian tên (namespace) của class đó, và nếu mang nó ra ngoài sẽ khiến người dùng API bối rối.
- Bạn đang muốn gom nhóm nó một cách chặt chẽ với các class method cụ thể khác.
Ví dụ: Cứ dùng hàm bình thường
# Đầu óc tư duy Java áp lên code Python
class MathUtils:
@staticmethod
def calculate_tax(amount: float) -> float:
return amount * 0.2
# Chuẩn phong cách Pythonic
def calculate_tax(amount: float) -> float:
return amount * 0.2
5) Khai báo rõ tham số vs chỉ dùng *args, **kwargs
Viết tham số rõ ràng khi:
- Bạn đang xây dựng một public API, service class, hay tầng logic lõi (business logic).
- Tính khám phá cực kì quan trọng (developer khác gọi hàm cần biết chính xác nên truyền cái gì vào).
- Bạn muốn dùng type-checking hỗ trợ bởi IDE.
Dùng *args, **kwargs khi:
- Bạn viết wrapper, decorator, hoặc middleware trung gian, với mục đích mù táng (blind pass-through) đẩy mọi giá trị nhận được qua cho hàm kế tiếp.
- Bạn viết hàm con kế thừa (subclassing) và đẩy tất cả argument lên qua
super().__init__().
Ví dụ: Nỗi bực dọc từ **kwargs bị ẩn
# Kiểu thiết kế API dễ gây cáu
def create_customer(**kwargs):
# Hàm này nhận cái gì đây? email? name? hay first_name? Không ai biết cả!
db.save(kwargs)
# Thiết kế API tuyệt vời
def create_customer(email: str, name: str, phone: str | None = None):
db.save({"email": email, "name": name, "phone": phone})
Ví dụ: Use-case hoàn hảo cho kwargs
import time
def time_it(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs) # Blind pass-through. Quá hoàn hảo.
print(f"Mất {time.time() - start} giây")
return result
return wrapper
Bảng tra cứu thu gọn (Phiên bản 2)
Dict vs Dataclass:
- Key linh hoạt & payload không cấu trúc → Dict
- Schema cố định & IDE hỗ trợ → Dataclass
List vs Generator:
- Cần đếm len() & lặp nhiều vòng → List
- Dữ liệu khổng lồ & cần chạy step-by-step → Generator
If/else vs Try/except:
- Thường xuyên tịt ngòi & phí kiểm tra rẻ → If/else
- Chạy ngoan 99% & phòng chống race conditions → Try/except
Static method vs Function:
- Không cần
selfhaycls→ Đá nó ra khỏi class và biến thành Python function!
Explicit params vs **kwargs:
- Core logic & API lõi → Tham số rõ ràng (Explicit)
- Decorators & Wrappers ->
*args, **kwargs
Bài viết liên quan
-
Khi Nào Nên Dùng Class vs. Function Trong Python: Checklist Thiết Kế
Một danh sách kiểm tra tư duy (mental checklist) có thể lặp lại cho các lập trình viên Python để quyết định khi nào nên dùng class, function, instance state, và dependency injection.
-
MoneyPrinterV2: 18.000 Sao GitHub Và Cái Máy In Nội Dung Tự Động
Ollama viết kịch bản, KittenTTS đọc, Gemini vẽ hình, MoviePy ghép video. Bạn ngồi uống cà phê. YouTube Short tự lên kênh.
-
Giải Mã Super Agent Harness: Đi Sâu Vào DeerFlow Của Bytedance
Khám phá cách DeerFlow 2.0 chuyển mình từ một công cụ nghiên cứu chuyên sâu thành một hệ sinh thái agent toàn diện với sandbox, sub-agent và bộ nhớ dài hạn.
-
Giải thích về OpenBB: Nền tảng Dữ liệu Mở cho Nghiên cứu Đầu tư
Phân tích sâu về OpenBB, nền tảng mã nguồn mở giúp thống nhất các API dữ liệu tài chính thành một giao diện duy nhất cho lập trình viên Python, nhà phân tích và các AI agents.