dataclasses: lớp dữ liệu gọn trong Python
1. Tổng quan
Khi viết class chỉ để chứa dữ liệu (kiểu Order, Customer, Point…), bạn thường phải tự viết __init__, __repr__, __eq__ — toàn là code lặp đi lặp lại. Module dataclasses (có từ Python 3.7) giúp bạn sinh tự động các method này chỉ bằng một decorator. Đọc xong bài này, bạn sẽ biết cách dùng @dataclass để viết class dữ liệu gọn, validate dữ liệu với __post_init__, làm class immutable, và chuyển đổi qua dict/JSON.
2. Dataclass cơ bản là gì
@dataclass là decorator đọc type hint trong class, rồi tự sinh __init__, __repr__, __eq__ dựa trên các field đó.
from dataclasses import dataclass
@dataclass
class Customer:
name: str
email: str
age: int = 0
@dataclassquét các attribute có type hint (name,email,age) và coi đó là field.age: int = 0có giá trị mặc định, giống tham số mặc định trong hàm.- Bạn không cần viết
__init__— nó được sinh tự động theo đúng thứ tự field.
c1 = Customer("An", "an@example.com")
print(c1)
Customer(name='An', email='an@example.com', age=0)
__repr__ sinh ra tự động hiển thị đầy đủ tên field và giá trị, rất tiện khi debug — class thường không có sẵn điều này.
3. So sánh với class thường và namedtuple
Viết tay class tương đương sẽ dài hơn nhiều:
class CustomerPlain:
def __init__(self, name, email, age=0):
self.name = name
self.email = email
self.age = age
def __repr__(self):
return f"CustomerPlain(name={self.name!r}, email={self.email!r}, age={self.age!r})"
def __eq__(self, other):
if not isinstance(other, CustomerPlain):
return NotImplemented
return (self.name, self.email, self.age) == (other.name, other.email, other.age)
@dataclass thay bạn viết toàn bộ phần này. So với namedtuple, dataclass khác ở chỗ:
- Đối tượng mutable theo mặc định (bạn sửa được
c1.age = 20), namedtuple thì không. - Hỗ trợ type hint rõ ràng, dễ tích hợp với type checker như
mypy. - Hỗ trợ
__post_init__, giá trị mặc định phức tạp, kế thừa — những thứnamedtuplekhông làm tốt.
Lưu ý: Nếu bạn chỉ cần tuple bất biến nhẹ, không cần method,
namedtuplehoặctyping.NamedTuplevẫn là lựa chọn gọn hơn.
4. field() và giá trị mặc định mutable
Một lỗi rất hay gặp: dùng list/dict làm giá trị mặc định trực tiếp.
from dataclasses import dataclass, field
@dataclass
class Order:
order_id: str
items: list[str] = field(default_factory=list)
- Nếu viết
items: list[str] = [], Python sẽ báo lỗiValueError: mutable default ... is not allowed— vì list mặc định sẽ bị chia sẻ giữa mọi instance, dẫn tới bug khó tìm. field(default_factory=list)yêu cầu dataclass gọilist()mới cho mỗi instance.
o1 = Order("A1")
o2 = Order("A2")
o1.items.append("Sách")
print(o1.items, o2.items)
['Sách'] []
Hai object có items độc lập, đúng như mong đợi.
5. Validate dữ liệu với __post_init__
__init__ do dataclass sinh ra không cho bạn chèn logic validate trực tiếp — nhưng có hook __post_init__ chạy ngay sau khi __init__ xong.
from dataclasses import dataclass
@dataclass
class PaymentRequest:
amount: float
currency: str = "VND"
def __post_init__(self):
if self.amount <= 0:
raise ValueError("amount phải lớn hơn 0")
self.currency = self.currency.upper()
__post_init__được gọi tự động sau khi gán xong các field.- Ở đây ta validate
amountvà chuẩn hóacurrencythành chữ hoa.
try:
PaymentRequest(amount=-100)
except ValueError as e:
print(e)
amount phải lớn hơn 0
6. Class bất biến với frozen=True
Nếu muốn object không thể sửa sau khi tạo (giống record trong Java), dùng frozen=True.
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
frozen=Truekhiến__setattr__bị chặn — mọi lần gán sau khi khởi tạo sẽ raise lỗi.- Nhờ bất biến, object
frozencó thể dùng làm key trongdicthoặc phần tử trongset(vì dataclass tự sinh__hash__khifrozen=Truevàeq=True).
p = Point(1.0, 2.0)
try:
p.x = 5.0
except Exception as e:
print(type(e).__name__, e)
FrozenInstanceError cannot assign to field 'x'
Lưu ý:
frozen=Truechỉ chặn gán attribute cấp 1. Nếu field là list/dict (mutable), nội dung bên trong vẫn sửa được. Muốn bất biến thật sự, hãy dùngtuplehoặc kiểu immutable cho field đó.
7. Kế thừa dataclass
Dataclass hỗ trợ kế thừa bình thường như class Python, field của class con được nối tiếp sau field của class cha.
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
@dataclass
class Employee(Person):
salary: float = 0.0
Employeekế thừaname,agetừPerson, rồi thêmsalary.- Thứ tự tham số trong
__init__sinh ra là:name, age, salary— giữ nguyên thứ tự khai báo.
e = Employee("Bình", 30, salary=15_000_000)
print(e)
Employee(name='Bình', age=30, salary=15000000.0)
Lưu ý: Field không có default phải đứng trước field có default trong toàn bộ chuỗi kế thừa. Nếu class cha có field với default mà class con thêm field không default, Python sẽ báo
TypeErrorlúc định nghĩa class.
8. So sánh và sắp xếp với order=True
Mặc định dataclass chỉ sinh __eq__. Muốn so sánh <, >, <=, dùng order=True.
from dataclasses import dataclass
@dataclass(order=True)
class Product:
price: float
name: str = field(compare=False)
order=Truesinh thêm__lt__,__le__,__gt__,__ge__, so sánh theo thứ tự field (giống so sánh tuple).field(compare=False)loạinamera khỏi phép so sánh — chỉ so sánh theoprice.
products = [Product(50_000, "Bút"), Product(20_000, "Vở")]
print(sorted(products))
[Product(price=20000, name='Vở'), Product(price=50000, name='Bút')]
sorted() dùng __lt__ do dataclass sinh ra, không cần viết key=lambda p: p.price thủ công.
9. Chuyển đổi qua dict và tuple
dataclasses cung cấp hàm tiện ích asdict và astuple để serialize object.
from dataclasses import dataclass, asdict, astuple
@dataclass
class Address:
city: str
country: str
@dataclass
class Customer2:
name: str
address: Address
cust = Customer2("Chi", Address("Hà Nội", "Việt Nam"))
print(asdict(cust))
print(astuple(cust))
{'name': 'Chi', 'address': {'city': 'Hà Nội', 'country': 'Việt Nam'}}
('Chi', ('Hà Nội', 'Việt Nam'))
asdictđệ quy chuyển cả nested dataclass (Address) thành dict con — rất tiện để trả JSON qua API.astupletương tự nhưng trả tuple lồng tuple.
Lưu ý:
asdict/astupledeepcopy toàn bộ dữ liệu. Với object lớn hoặc chứa field không thể copy (file handle, connection…), gọi hàm này sẽ chậm hoặc lỗi.
10. Viết test cho dataclass
Vì __eq__ được sinh tự động theo giá trị field, test so sánh object rất tự nhiên với pytest.
import pytest
from dataclasses import FrozenInstanceError
def test_customer_equality():
c1 = Customer("An", "an@example.com", 20)
c2 = Customer("An", "an@example.com", 20)
assert c1 == c2
def test_payment_request_rejects_non_positive_amount():
with pytest.raises(ValueError):
PaymentRequest(amount=0)
def test_point_is_frozen():
p = Point(1.0, 2.0)
with pytest.raises(FrozenInstanceError):
p.x = 10.0
test_customer_equalitykiểm tra hai object khác instance nhưng cùng giá trị field vẫn==nhau — đúng nhờ__eq__tự sinh.test_payment_request_rejects_non_positive_amountkiểm tra__post_init__validate đúng.test_point_is_frozenxác nhậnfrozen=Truechặn gán attribute.
11. Lỗi hay gặp và khi nào không nên dùng
- Quên
default_factorycho mutable default: dùng= []hoặc= {}trực tiếp sẽ lỗi ngay khi khai báo class, phải dùngfield(default_factory=...). - Trộn field có default và không default sai thứ tự khi kế thừa: gây
TypeErrorlúc load module, khó debug nếu class nằm rải ở nhiều file. - Tưởng
frozen=Truelà deep immutable: field kiểu list/dict bên trong vẫn sửa được nội dung, chỉ không gán lại attribute. - Dùng dataclass cho logic nghiệp vụ phức tạp: nếu class có nhiều method xử lý, quan hệ phức tạp với object khác, dataclass chỉ giúp phần khởi tạo/so sánh — phần logic bạn vẫn phải tự viết, đừng lạm dụng để “giả vờ” class đó đơn giản hơn thực tế.
- Cần tương thích Python < 3.7:
dataclasseskhông có sẵn, phải cài packagedataclassestừ PyPI (backport) hoặc dùngattrs.
Lưu ý: Nếu cần validate phức tạp, custom serialization, hoặc tích hợp sâu với JSON schema, các thư viện như
pydanticthường mạnh hơndataclassesthuần — đáng xem xét khi project lớn.
12. Kết luận
dataclasses giúp bạn loại bỏ phần lớn boilerplate khi viết class chứa dữ liệu: tự sinh __init__, __repr__, __eq__, hỗ trợ validate qua __post_init__, bất biến qua frozen=True, và serialize dễ dàng qua asdict/astuple. Với các class dữ liệu đơn giản tới trung bình, đây là lựa chọn gọn và dễ đọc hơn nhiều so với viết tay. Khi nhu cầu validate hoặc serialize phức tạp hơn, bạn có thể cân nhắc pydantic hoặc attrs thay thế.