Giới thiệu về module dataclasses
Module dataclasses được giới thiệu trong Python 3.7, cung cấp một tập hợp các decorator và hàm để đơn giản hóa việc định nghĩa các class, đặc biệt hữu ích khi bạn cần tạo các class chủ yếu để lưu trữ dữ liệu. Module này tự động sinh các phương thức đặc biệt như __init__, __repr__, __eq__, giúp giảm thiểu đáng kể lượng mã lệnh lặp đi lặp lại.
Ví dụ, một class thông thường yêu cầu khai báo như sau:
class NguoiDung:
def __init__(self, ten: str, tuoi: int):
self.ten = ten
self.tuoi = tuoi
def __repr__(self):
return f"NguoiDung(ten={self.ten}, tuoi={self.tuoi})"
def __eq__(self, other):
if not isinstance(other, NguoiDung):
return False
return self.ten == other.ten and self.tuoi == other.tuoi
Khi sử dụng dataclasses, code có thể được rút gọn thành:
from dataclasses import dataclass
@dataclass
class NguoiDung:
ten: str
tuoi: int
Code trên không chỉ ngắn gọn hơn mà còn cung cấp đầy đủ các phương thức tự động được sinh ra.
Các tham số của decorator @dataclass
Decorator @dataclass hỗ trợ nhiều tham số tùy chọn để kiểm soát hành vi của class được tạo:
@dataclass(
init: bool = True,
repr: bool = True,
eq: bool = True,
order: bool = False,
unsafe_hash: bool = False,
frozen: bool = False,
match_args: bool = True,
kw_only: bool = False,
slots: bool = False
)
1. init: bool = True
Tham số này xác định có sinh tự động phương thức __init__ hay không.
- Giá trị mặc định là
True, nghĩa là phương thức khởi tạo sẽ được tự động tạo dựa trên các trường (fields) được định nghĩa trong class. - Nếu đặt là
False, bạn cần tự định nghĩa phương thức__init__hoặc sử dụng cách khác để khởi tạo đối tượng.
Ví dụ minh họa:
@dataclass(init=False)
class NguoiDung:
ten: str
tuoi: int
def __init__(self):
self.ten = "Không xác định"
self.tuoi = 0
nguoi = NguoiDung()
print(nguoi) # NguoiDung(ten=Không xác định, tuoi=0)
2. repr: bool = True
Tham số này kiểm soát việc sinh phương thức __repr__.
- Khi
True, một chuỗi biểu diễn đối tượng bao gồm tên class và giá trị các trường sẽ được tự động tạo. - Khi
False, phương thức__repr__sẽ không được sinh ra.
Ví dụ minh họa:
@dataclass(repr=False)
class NguoiDung:
ten: str
tuoi: int
nguoi = NguoiDung("Minh", 25)
print(nguoi) # <__main__.NguoiDung object at 0x...>
3. eq: bool = True
Tham số này xác định có sinh phương thức __eq__ hay không.
- Với
True, phương thức so sánh bằng sẽ so sánh tất cả các trường của hai đối tượng. - Với
False, việc so sánh sẽ sử dụng cách so sánh theo danh tính (identity comparison) mặc định của Python.
Ví dụ minh họa:
@dataclass(eq=False)
class NguoiDung:
ten: str
tuoi: int
nd1 = NguoiDung("Minh", 25)
nd2 = NguoiDung("Minh", 25)
print(nd1 == nd2) # False (so sánh theo identity)
4. order: bool = False
Tham số này kiểm soát việc sinh các phương thức so sánh: __lt__, __le__, __gt__, __ge__.
- Khi
True, các phương thức so sánh sẽ được sinh ra, cho phép so sánh các đối tượng dựa trên giá trị các trường. - Lưu ý: Nếu
order=Truethìeqcũng phải làTrue.
Ví dụ minh họa:
@dataclass(order=True)
class NguoiDung:
ten: str
tuoi: int
nd1 = NguoiDung("Minh", 25)
nd2 = NguoiDung("Lan", 30)
print(nd1 < nd2) # True (so sánh theo thứ tự trường: ten trước)
5. unsafe_hash: bool = False
Tham số này xác định có sinh phương thức __hash__ hay không, ngay cả khi class có thể thay đổi được.
- Theo mặc định,
__hash__chỉ được sinh khi class là bất biến (frozen=True). - Cần thận trọng khi sử dụng
unsafe_hash=Truevì có thể dẫn đến hành vi không an toàn.
Ví dụ minh họa:
@dataclass(unsafe_hash=True)
class NguoiDung:
ten: str
tuoi: int
nd = NguoiDung("Minh", 25)
print(hash(nd)) # Sinh giá trị hash
6. frozen: bool = False
Tham số này xác định class có bất biến (immutable) hay không.
- Khi
True, các trường của đối tượng không thể thay đổi sau khi khởi tạo, mọi nỗ lực sửa đổi sẽ gây raFrozenInstanceError. - Khi
frozen=True, phương thức__hash__sẽ được tự động sinh ra.
Ví dụ minh họa:
@dataclass(frozen=True)
class NguoiDung:
ten: str
tuoi: int
nd = NguoiDung("Minh", 25)
nd.tuoi = 26 # Gây ra dataclasses.FrozenInstanceError
7. match_args: bool = True
Tham số này kiểm soát việc sinh __match_args__ để hỗ trợ pattern matching trong Python 3.10+.
- Khi
True, tuple__match_args__chứa tên các trường sẽ được tự động tạo.
Ví dụ minh họa:
@dataclass(match_args=True)
class NguoiDung:
ten: str
tuoi: int
nd = NguoiDung("Minh", 25)
match nd:
case NguoiDung(ten, tuoi):
print(f"Tên: {ten}, Tuổi: {tuoi}") # Tên: Minh, Tuổi: 25
8. kw_only: bool = False
Tham số này xác định các tham số của __init__ có bắt buộc phải là keyword arguments hay không.
- Khi
True, tất cả các trường phải được truyền bằng keyword arguments. - Khi
False, có thể truyền theo vị trí hoặc theo tên.
Ví dụ minh họa:
@dataclass(kw_only=True)
class NguoiDung:
ten: str
tuoi: int
nd = NguoiDung(ten="Minh", tuoi=25) # Hợp lệ
nd = NguoiDung("Minh", 25) # Gây ra TypeError
9. slots: bool = False
Tham số này kiểm soát việc sử dụng __slots__ thay vì __dict__ để lưu trữ các trường.
- Khi
True, class sử dụng__slots__, giới hạn các thuộc tính chỉ có trong các trường được định nghĩa, giảm memory usage và tăng tốc độ truy cập. - Khi
False, sử dụng__dict__, cho phép thêm thuộc tính động.
Ví dụ minh họa:
@dataclass(slots=True)
class NguoiDung:
ten: str
tuoi: int
nd = NguoiDung("Minh", 25)
nd.truong_moi = "test" # Gây ra AttributeError
Định nghĩa và sử dụng Fields
Các trường trong dataclass được định nghĩa thông qua type annotations. Hàm dataclasses.field() cho phép tùy chỉnh thêm hành vi của từng trường.
Cú pháp:
dataclasses.field(*, default, default_factory, init, repr, hash, compare, metadata, kw_only)
Các tham số của field():
default: Giá trị mặc định cho trường.default_factory: Hàm không tham số trả về giá trị mặc định (dùng cho các kiểu mutable như list).init: Trường có được đưa vào__init__không (mặc định True).repr: Trường có hiển thị trong__repr__không (mặc định True).hash: Trường có được dùng trong__hash__không.compare: Trường có dùng trong so sánh không (mặc định True).metadata: Metadata bổ sung (dictionary tùy chỉnh).kw_only: Trường có bắt buộc dùng keyword argument không.
Ví dụ sử dụng field():
from dataclasses import dataclass, field
from typing import List
@dataclass
class SinhVien:
ten: str
tuoi: int = 0
diem: List[int] = field(default_factory=list)
ma: int = field(init=False, default=100)
sv = SinhVien("Minh")
print(sv) # SinhVien(ten='Minh', tuoi=0, diem=[], ma=100)
Các tính năng bổ sung và sử dụng nâng cao
1. Phương thức __post_init__
Để thực hiện logic bổ sung sau khi khởi tạo, có thể định nghĩa phương thức __post_init__:
@dataclass
class NguoiDung:
ten: str
tuoi: int
thong_tin: str = field(init=False)
def __post_init__(self):
self.thong_tin = f"{self.ten} - {self.tuoi} tuổi"
nd = NguoiDung("Minh", 25)
print(nd.thong_tin) # Minh - 25 tuổi
2. Kế thừa dataclass
Dataclass hỗ trợ kế thừa, nhưng cần chú ý đến thứ tự khởi tạo các trường:
@dataclass
class NguoiDung:
ten: str
tuoi: int
@dataclass
class NhanVien(NguoiDung):
luong: float
nv = NhanVien("Minh", 25, 5000000)
print(nv) # NhanVien(ten='Minh', tuoi=25, luong=5000000.0)
3. Chuyển đổi sang dictionary hoặc tuple
Module cung cấp các hàm hỗ trợ:
asdict(obj): Chuyển đổi đối tượng dataclass thành dictionary.astuple(obj): Chuyển đổi đối tượng dataclass thành tuple.
Ví dụ:
from dataclasses import asdict, astuple
nd = NguoiDung("Minh", 25)
print(asdict(nd)) # {'ten': 'Minh', 'tuoi': 25}
print(astuple(nd)) # ('Minh', 25)
Ưu điểm và hạn chế
Ưu điểm
- Ngắn gọn: Giảm mã lệnh lặp đi lặp lại, tự động sinh các phương thức thông dụng.
- An toàn kiểu: Kết hợp với type annotations, tăng cường khả năng đọc và bảo trì code.
- Linh hoạt: Cung cấp nhiều tùy chọn cấu hình thông qua các tham số và
field(). - Hiệu năng: Hỗ trợ
__slots__để tối ưu bộ nhớ và tốc độ truy cập.
Hạn chế
- Đường cong học tập: Cần thời gian để hiểu các tham số và cách sử dụng
field(). - Logic phức tạp: Không phù hợp với các class yêu cầu logic khởi tạo phức tạp.
- Vấn đề kế thừa: Khi kế thừa nhiều dataclass, cần quản lý cẩn thận thứ tự trường và tham số
__init__.
Kết luận
Module dataclasses là công cụ mạnh mẽ trong Python để tạo các class lưu trữ dữ liệu. Thông qua decorator @dataclass và hàm field(), lập trình viên có thể giảm đáng kể lượng mã lệnh lặp đi lặp lại, đồng thời được cung cấp nhiều tùy chọn cấu hình linh hoạt. Các tham số như init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, và slots cho phép kiểm soát chính xác hành vi của class, phù hợp với nhiều tình huống từ đơn giản đến phức tạp.