Python dataclasses: Hướng dẫn sử dụng module tạo class lưu trữ dữ liệu

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=True thì eq cũ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=True vì 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 ra FrozenInstanceError.
  • 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.

Thẻ: python dataclasses python-3.7 oop class-design

Đăng vào ngày 16 tháng 6 lúc 21:04