Python Dataclasses Nâng Cao: Đừng Chỉ Dùng Để Khai Báo Biến

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Thoát khỏi đống boilerplate nhàm chán

Làm dự án Python lớn mà phải viết đi viết lại __init__, __repr__ hay __eq__ cho cả trăm class thì cực kỳ nản. Nó vừa tốn công, vừa dễ sai sót. Trước bản 3.7, chúng ta thường phải viết những đoạn code dài dằng dặc chỉ để gán giá trị cho thuộc tính:

class User:
    def __init__(self, id: int, name: str, email: str):
        self.id = id
        self.name = name
        self.email = email

    def __repr__(self):
        return f"User(id={self.id}, name='{self.name}', email='{self.email}')"

Nếu class có 20 thuộc tính, file code của bạn sẽ đầy rác. @dataclass ra đời để dọn dẹp đống hỗn độn đó. Tuy nhiên, nếu chỉ dùng decorator này ở mức cơ bản, bạn sẽ sớm va phải những ca khó trong môi trường production. Ví dụ: làm sao để chặn lỗi dùng chung bộ nhớ khi đặt giá trị mặc định là list? Hay làm sao để tính toán dữ liệu ngay khi vừa khởi tạo?

Cảnh giác với lỗi Mutable Default

Dữ liệu thực tế thường lắt léo hơn lý thuyết. Một sai lầm kinh điển mà ngay cả các senior cũng đôi khi mắc phải là gán trực tiếp list hoặc dict làm giá trị mặc định.

Thử nhìn dòng này: tags: list = []. Trong Python, mọi instance của class này sẽ dùng chung đúng một cái list đó. Mình từng mất hơn 4 tiếng đồng hồ chỉ để debug một hệ thống xử lý log vì lỗi này. Dữ liệu của user A cứ nhảy sang user B một cách khó hiểu. Để an toàn, hãy luôn dùng field(default_factory=...).

Tùy biến thuộc tính tinh tế với hàm field()

Hàm field() là công cụ mạnh nhất trong module dataclasses. Nó giúp bạn kiểm soát chi tiết cách từng thuộc tính vận hành.

1. Khởi tạo list/dict an toàn

Sử dụng default_factory để đảm bảo mỗi khi tạo object mới, Python sẽ cấp phát một vùng nhớ riêng biệt.

from dataclasses import dataclass, field
from typing import List

@dataclass
class Product:
    name: str
    price: float
    tags: List[str] = field(default_factory=list)
    metadata: dict = field(default_factory=dict)

2. Bảo mật thông tin khi in Log

Khi debug, chúng ta hay in object ra để kiểm tra. Nhưng bạn chắc chắn không muốn mật khẩu hay API token hiện lù lù trên hệ thống log. Chỉ cần thêm repr=False, thuộc tính đó sẽ biến mất khỏi hàm print nhưng vẫn dùng được bình thường.

@dataclass
class Account:
    username: str
    password: str = field(repr=False) # Ẩn đi để bảo mật

Xử lý logic thông minh với __post_init__

Hàm __post_init__ sẽ tự động chạy ngay sau khi object được tạo xong. Đây là nơi lý tưởng để validate dữ liệu hoặc tính toán các trường phụ thuộc.

Giả sử bạn cần tính tổng tiền đơn hàng và kiểm tra định dạng email khách hàng:

import re
from dataclasses import dataclass, field

@dataclass
class Order:
    item_name: str
    unit_price: float
    quantity: int
    customer_email: str
    total_price: float = field(init=False) # Không cho phép truyền từ ngoài vào

    def __post_init__(self):
        self.total_price = self.unit_price * self.quantity
        
        if self.quantity <= 0:
            raise ValueError("Số lượng phải lớn hơn 0")
            
        # Kiểm tra email bằng Regex
        # Nếu cần test nhanh regex pattern, bạn có thể dùng toolcraft.app/vi/tools/developer/regex-tester để check kết quả ngay trên trình duyệt
        email_regex = r'^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,3}$'
        if not re.match(email_regex, self.customer_email):
            raise ValueError(f"Email {self.customer_email} không hợp lệ")

Việc đặt init=False cực kỳ quan trọng. Nó khẳng định total_price là giá trị nội bộ, giúp tránh việc người dùng nhập sai dữ liệu từ bên ngoài.

Serialization: Đưa dữ liệu lên API

Sau khi xử lý xong, bạn thường phải biến object thành JSON để trả về cho Client hoặc lưu vào DB. Python có sẵn hàm asdict giúp việc này trở nên cực kỳ nhẹ nhàng.

from dataclasses import dataclass, asdict
import json

@dataclass
class InventoryItem:
    name: str
    unit_price: float
    quantity: int

item = InventoryItem("MacBook Pro", 2500.0, 10)
# Chuyển thành dictionary trong 1 nốt nhạc
item_dict = asdict(item)
print(json.dumps(item_dict))

Lưu ý nhỏ: Nếu class có chứa kiểu datetime, hàm json.dumps sẽ báo lỗi. Lúc này, bạn nên cân nhắc dùng các thư viện chuyên sâu như pydantic hoặc tự viết custom encoder.

Kinh nghiệm thực chiến: Dataclass hay Pydantic?

Nhiều bạn thường phân vân không biết nên chọn cái nào. Dựa trên kinh nghiệm của mình:

  • Chọn Dataclasses: Khi bạn cần cấu trúc dữ liệu gọn nhẹ, có sẵn trong Python core, chủ yếu dùng nội bộ trong logic xử lý.
  • Chọn Pydantic: Khi làm API (FastAPI), cần parse JSON phức tạp hoặc yêu cầu ép kiểu (type casting) nghiêm ngặt.

Một mẹo nhỏ để code an toàn hơn: Hãy dùng frozen=True. Nó biến object thành Immutable (không thể sửa đổi sau khi tạo). Nếu ai đó cố tình gán lại giá trị, Python sẽ ném lỗi FrozenInstanceError ngay lập tức, giúp tránh các bug side-effect cực kỳ khó chịu.

@dataclass(frozen=True)
class AppConfig:
    db_host: str = "localhost"
    port: int = 5432

Tóm lại, nắm vững field()__post_init__ sẽ giúp bạn nâng tầm từ một coder bình thường lên chuyên nghiệp. Đừng chỉ coi dataclass là cái túi chứa dữ liệu, hãy biến nó thành lớp bảo vệ dữ liệu vững chắc cho dự án của bạn.

Share: