Featured image of post Khi Nào Nên Dùng Dataclass, Generator và Try/Except Trong Python

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) hay dataclass?
  • 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ùng try/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

1
2
3
# 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

1
2
3
4
5
6
7
8
9
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ớ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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ể) hay cls (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ắnkhắ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

1
2
3
4
5
6
7
8
9
# Đầ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

1
2
3
4
5
6
7
8
# 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

1
2
3
4
5
6
7
8
9
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 self hay clsĐá 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
Được tạo với sự lười biếng tình yêu 🦥

Subscribe to My Newsletter