Data class là một trong những tính năng của Python mà một khi bạn đã biết đến, bạn sẽ chẳng muốn quay lại cách cũ nữa. Hãy xem lớp thông thường sau:
classExercise:def__init__(self, name, reps, sets, weight):
self.name = name
self.reps = reps
self.sets = sets
self.weight = weight
Được hỗ trợ bởi
Với tôi, định nghĩa lớp này rất kém hiệu quả — trong phương thức __init__, bạn lặp lại mỗi tham số ít nhất ba lần. Nghe có vẻ không nghiêm trọng, nhưng hãy nghĩ tới tần suất bạn viết các lớp trong suốt sự nghiệp với nhiều tham số hơn thế.
So sánh với lựa chọn data class cho đoạn mã trên:
from dataclasses import dataclass
@dataclassclassExercise:
name:str
reps:int
sets:int
weight:float# Weight in lbsĐược hỗ trợ bởi
Đoạn mã trông khiêm tốn này tốt hơn lớp thông thường rất nhiều lần. Decorator nhỏ @dataclass sẽ tự động triển khai các phương thức __init__, __repr__, __eq__ ở phía sau, vốn sẽ tốn ít nhất 20 dòng nếu viết tay.
Ngoài ra, nhiều tính năng khác như toán tử so sánh, sắp xếp đối tượng và tính bất biến đều chỉ cách một dòng để “tự sinh” cho lớp của chúng ta.
Vì vậy, mục tiêu của hướng dẫn này là cho bạn thấy vì sao data class là một trong những điều tuyệt vời nhất xảy đến với Python nếu bạn yêu thích lập trình hướng đối tượng.
Bắt đầu thôi!
Những điều cơ bản về Data Class trong Python
Hãy điểm qua một số khái niệm nền tảng của data class trong Python khiến chúng hữu ích đến vậy.
Một số phương thức được tạo tự động trong data class
Bất chấp mọi tính năng, data class vẫn là các lớp thông thường nhưng cần ít mã hơn nhiều để triển khai cùng chức năng. Đây là lớp Exercise một lần nữa:
from dataclasses import dataclass
@dataclassclassExercise:
name:str
reps:int
sets:int
weight:float
ex1 = Exercise("Bench press",10,3,52.5)# Verifying Exercise is a regular class
ex1.name
'Bench press'Được hỗ trợ bởi
Hiện tại, Exercise đã có sẵn các phương thức __repr__ và __eq__. Hãy kiểm tra:
repr(ex1)"Exercise(name='Bench press', reps=10, sets=3, weight=52.5)"Được hỗ trợ bởi
Biểu diễn đối tượng của repr phải trả về đoạn mã có thể tái tạo chính nó, và ta thấy điều đó đúng hệt với ex1.
Trong khi đó, Exercise định nghĩa theo cách cũ sẽ trông như sau:
classExercise:def__init__(self, name, reps, sets, weight):
self.name = name
self.reps = reps
self.sets = sets
self.weight = weight
ex3 = Exercise("Bench press",10,3,52.5)
ex3
<__main__.Exercise at 0x7f6834100130>Được hỗ trợ bởi
Trông khá tệ và vô dụng!
Giờ hãy kiểm tra sự tồn tại của __eq__, tức toán tử bằng nhau:
# Redefine the class@dataclassclassExercise:
name:str
reps:int
sets:int
weight:float
ex1 = Exercise("Bench press",10,3,52.5)
ex2 = Exercise("Bench press",10,3,52.5)Được hỗ trợ bởi
So sánh lớp với chính nó và với một lớp khác có tham số giống hệt nhau phải trả về True:
Và đúng là như vậy! Với các lớp thông thường, viết logic này sẽ rất phiền phức.
Data class yêu cầu gợi ý kiểu
Như bạn có thể thấy, data class yêu cầu gợi ý kiểu khi định nghĩa trường. Thực tế, data class chấp nhận mọi kiểu từ mô-đun typing. Ví dụ, đây là cách tạo một trường có thể nhận kiểu dữ liệu Any:
from typing import Any
@dataclassclassDummy:
attr: AnyĐược hỗ trợ bởi
Tuy nhiên, “cá tính” của Python là dù data class yêu cầu gợi ý kiểu, các kiểu lại không thực sự bị ép buộc.
Chẳng hạn, tạo một thể hiện của lớp Exercise với kiểu dữ liệu hoàn toàn sai vẫn chạy không lỗi:
Trong thực tế, bạn hiếm khi định nghĩa mặc định bằng cú pháp name: type = value.
Thay vào đó, bạn sẽ dùng hàm field, cho phép kiểm soát nhiều hơn đối với từng định nghĩa trường:
from dataclasses import field
@dataclassclassExercise:
name:str= field(default="Push-up")
reps:int= field(default=10)
sets:int= field(default=3)
weight:float= field(default=0)# Now, all fields have defaults
ex5 = Exercise()
ex5
Exercise(name='Push-up', reps=10, sets=3, weight=0)Được hỗ trợ bởi
Hàm field có thêm nhiều tham số, chẳng hạn:
repr
init
compare
default_factory
v.v. Chúng ta sẽ bàn tới trong các phần sau.
Có thể tạo data class bằng một hàm
Một ghi chú cuối về cơ bản của data class là định nghĩa của chúng còn có thể ngắn hơn nữa với hàm make_dataclass:
from dataclasses import make_dataclass
Exercise = make_dataclass("Exercise",[("name",str),("reps",int),("sets",int),("weight",float),],)
ex3 = Exercise("Deadlifts",8,3,69.0)
ex3
Exercise(name='Deadlifts', reps=8, sets=3, weight=69.0)Được hỗ trợ bởi
Nhưng bạn sẽ phải đánh đổi tính dễ đọc, nên tôi không khuyến nghị dùng hàm này.
Data Class nâng cao trong Python
Trong phần này, chúng ta sẽ bàn về các tính năng nâng cao của data class mang lại nhiều lợi ích hơn. Một trong số đó là default factory.
Default factory
Để giải thích default factory, hãy tạo một lớp khác tên là WorkoutSession nhận hai trường:
from dataclasses import dataclass
from typing import List
@dataclassclassExercise:
name:str="Push-ups"
reps:int=10
sets:int=3
weight:float=0@dataclassclassWorkoutSession:
exercises: List[Exercise]
duration_minutes:intĐược hỗ trợ bởi
Bằng cách dùng kiểu List, ta chỉ định rằng WorkoutSession nhận một danh sách các thể hiện Exercise.
# Define the Exercise instances for HIIT training
ex1 = Exercise(name="Burpees", reps=15, sets=3)
ex2 = Exercise(name="Mountain Climbers", reps=20, sets=3)
ex3 = Exercise(name="Jump Squats", reps=12, sets=3)
exercises_monday =[ex1, ex2, ex3]
hiit_monday = WorkoutSession(exercises=exercises_monday, duration_minutes=30)Được hỗ trợ bởi
Hiện tại, mỗi thể hiện buổi tập đều cần có bài tập khi khởi tạo. Nhưng điều này không phản ánh cách mọi người tập — trước tiên họ bắt đầu một buổi (có lẽ trong ứng dụng), rồi thêm bài tập khi tập.
Vì vậy, chúng ta cần có thể tạo buổi tập không có bài tập và không có thời lượng. Hãy làm điều này bằng cách thêm danh sách rỗng làm mặc định cho exercises:
@dataclassclassWorkoutSession:
exercises: List[Exercise]=[]
duration_minutes:int=None
hiit_monday = WorkoutSession("25-02-2024")
ValueError: mutable default <class'list'>for field exercises isnot allowed: use default_factoryĐược hỗ trợ bởi
Tuy nhiên, ta gặp lỗi — hóa ra data class không cho phép giá trị mặc định có thể thay đổi (mutable).
May mắn là ta có thể sửa bằng cách dùng default factory:
Tham số default_factory nhận một hàm trả về giá trị khởi tạo cho một trường của data class. Tức là nó có thể nhận bất kỳ hàm tùy ý nào:
tuple
dict
set
Bất kỳ hàm tùy biến do người dùng định nghĩa
Điều này đúng bất kể kết quả của hàm là có thể thay đổi hay không.
Giờ nghĩ kỹ, hầu hết mọi người bắt đầu tập bằng các bài khởi động khá giống nhau cho mọi kiểu buổi tập. Vậy khởi tạo buổi tập mà không có bài tập có thể không đúng với một số người.
Thay vào đó, hãy tạo một hàm trả về ba Exercise khởi động:
defcreate_warmup():return[
Exercise("Jumping jacks",30,1),
Exercise("Squat lunges",10,2),
Exercise("High jumps",20,1),]@dataclassclassWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5# Increase the default duration as well
hiit_monday = WorkoutSession()
hiit_monday
WorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), Exercise(name='Squat lunges', reps=10, sets=2, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0)], duration_minutes=5)Được hỗ trợ bởi
Giờ đây, mỗi khi tạo buổi tập, chúng sẽ đi kèm một vài bài khởi động đã được ghi lại. Phiên bản mới của WorkoutSession có thời lượng mặc định năm phút để phản ánh điều đó.
Thêm phương thức vào data class
Vì data class là các lớp thông thường, việc thêm phương thức không thay đổi. Hãy thêm hai phương thức cho data class WorkoutSession của chúng ta:
Sử dụng các phương thức này, giờ ta có thể ghi lại mọi hoạt động mới vào một buổi tập:
hiit_monday = WorkoutSession()# Log a new exercise
new_exercise = Exercise("Deadlifts",6,4,60)
hiit_monday.add_exercise(new_exercise)
hiit_monday.increase_duration(15)Được hỗ trợ bởi
Biểu diễn dạng chuỗi của lớp dễ chịu hơn nhiều. Giờ hãy sửa cả WorkoutSession:
@dataclassclassWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5# Increase the default duration as welldefadd_exercise(self, exercise: Exercise):
self.exercises.append(exercise)defincrease_duration(self, minutes:int):
self.duration_minutes += minutes
def__str__(self):
base =""for ex in self.exercises:
base +=str(ex)+"\n"
base +=f"\nSession duration: {self.duration_minutes} minutes."return base
hiit_monday = WorkoutSession()print(hiit_monday)
Jumping jacks:30/1
Squat lunges:10/2
High jumps:20/1
Session duration:5 minutes.Được hỗ trợ bởi
Lưu ý: Hãy dùng nút “Giải thích mã” ở cuối đoạn để xem giải thích từng dòng.
Giờ chúng ta đã có đầu ra gọn gàng và dễ đọc.
So sánh trong data class
Với nhiều lớp, việc so sánh các đối tượng theo một logic nào đó là hợp lý. Với các buổi tập, có thể so sánh theo thời lượng, cường độ bài tập hoặc mức tạ.
Trước hết, hãy xem điều gì xảy ra nếu ta thử so sánh hai buổi tập ở trạng thái hiện tại:
hiit_wednesday = WorkoutSession()
hiit_wednesday.add_exercise(Exercise("Pull-ups",7,3))print(hiit_wednesday)
Jumping jacks:30/1
Squat lunges:10/2
High jumps:20/1
Pull-ups:7/3
Session duration:5 minutes.
hiit_monday > hiit_wednesday
TypeError:'>'not supported between instances of 'WorkoutSession'and'WorkoutSession'Được hỗ trợ bởi
Ta nhận TypeError vì data class không triển khai toán tử so sánh. Nhưng có thể khắc phục dễ dàng bằng cách đặt tham số order thành True:
Lần này phép so sánh hoạt động, nhưng ta đang so sánh cái gì?
Trong data class, phép so sánh được thực hiện theo thứ tự các trường được định nghĩa. Lúc này, các lớp được so sánh dựa trên thời lượng buổi tập vì trường đầu tiên, exercises, chứa các đối tượng không chuẩn.
Ta có thể kiểm tra bằng cách tăng thời lượng buổi tập thứ Tư:
Bằng cách đặt compare thành False cho bất kỳ trường nào, ta loại trừ nó khỏi việc so sánh, như kết quả trên cho thấy.
Xử lý trường sau khởi tạo (post-init)
Hiện tại, ta đặt thời lượng mặc định là năm phút để tính cho bài khởi động. Tuy nhiên, điều này chỉ hợp lý nếu người dùng bắt đầu bằng khởi động. Nếu họ bắt đầu bằng bài khác thì sao:
new_session = WorkoutSession([Exercise("Diamond push-ups",10,3)])
new_session.duration_minutes
5Được hỗ trợ bởi
Với chỉ một bài tập, tổng thời lượng là năm phút là không hợp lý. Mỗi buổi tập phải ước tính thời lượng động dựa trên số set của mỗi bài. Tức là ta nên làm cho duration_minutes phụ thuộc vào trường exercises.
Hãy triển khai:
@dataclassclassWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int= field(default=0, init=False)def__post_init__(self):
set_duration =3for ex in self.exercises:
self.duration_minutes += ex.sets * set_duration
...Được hỗ trợ bởi
Lần này, ta định nghĩa duration_minutes với init đặt là False để trì hoãn khởi tạo trường.
Sau đó, trong phương thức đặc biệt __post_init__, ta cập nhật giá trị dựa trên tổng số set của mỗi Exercise.
Giờ khi khởi tạo WorkoutSession, duration_minutes sẽ tăng động thêm ba phút cho mỗi set trong mỗi bài tập.
# Adding an exercise with three sets
hiit_friday = WorkoutSession([Exercise("Diamond push-ups",10,3)])
hiit_friday.duration_minutes
9Được hỗ trợ bởi
Nhìn chung, nếu bạn muốn định nghĩa một trường phụ thuộc các trường khác của data class, bạn có thể dùng logic __post_init__.
Tính bất biến trong Data Class
Data class WorkoutSession của chúng ta gần như sẵn sàng; chỉ còn cần được bảo vệ. Hiện tại, nó có thể bị sửa bừa rất dễ:
hiit_friday.duration_minutes =1000
hiit_friday.duration_minutes
1000del hiit_friday.exercisesĐược hỗ trợ bởi
Chúng ta muốn bảo vệ mọi trường của lớp để chỉ có thể sửa theo cách ta mong muốn. Để làm điều này, decorator @dataclass cung cấp tham số tiện lợi frozen:
Giờ nếu muốn sửa bất kỳ trường nào, ta sẽ nhận lỗi:
ex1.sets =5
FrozenInstanceError: cannot assign to field 'sets'Được hỗ trợ bởi
Đặt frozen là True sẽ tự động thêm các phương thức __deleteattr__ và __setattr__ cho mỗi trường để ngăn xóa hoặc cập nhật sau khi khởi tạo. Ngoài ra, người khác cũng không thể thêm trường mới:
ex1.new_field =10
FrozenInstanceError: cannot assign to field 'new_field'Được hỗ trợ bởi
Chức năng này sẽ tốn hàng chục dòng nếu chúng ta dùng lớp truyền thống.
Tuy nhiên, lưu ý rằng ta không thể làm cho lớp thực sự bất biến. Ví dụ, hãy viết lại WorkoutSession với frozen là True:
Tuy nhiên, exercises là một danh sách, hoàn toàn có thể thay đổi, khiến thao tác sau khả thi:
# Thay đổi một phần tử trong danh sách
# Changing one of the elements in a list
session1.exercises[1]= FrozenExercise("Totally new exercise",5,5)print(session1)
ImmutableWorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), FrozenExercise(name='Totally new exercise', reps=5, sets=5, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0)], duration_minutes=5)Được hỗ trợ bởi
Vì vậy, để tránh thay đổi ngoài ý muốn, khuyến nghị dùng các đối tượng bất biến như tuple cho giá trị trường.
Kế thừa trong data class
Điểm cuối cùng chúng ta sẽ đề cập là thứ tự các trường trong lớp cha và lớp con.
Vì data class là các lớp thông thường, kế thừa hoạt động như bình thường:
@dataclass(frozen=True)classImmutableWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5@dataclass(frozen=True)classCardioWorkoutSession(ImmutableWorkoutSession):passĐược hỗ trợ bởi
Nhưng vì trường cuối trong lớp cha (ImmutableWorkoutSession) có giá trị mặc định, tất cả trường trong lớp con cũng phải có giá trị mặc định.
Ví dụ, điều này là không được phép:
@dataclass(frozen=True)classImmutableWorkoutSession:
exercises: List[Exercise]= field(default_factory=create_warmup)
duration_minutes:int=5@dataclass(frozen=True)classCardioWorkoutSession(ImmutableWorkoutSession):
intensity_level:str# Not allowed, must have a default
TypeError: non-default argument 'intensity_level' follows default argument
Được hỗ trợ bởi
Nhược điểm của Data Class và Tài nguyên tham khảo
Data class đã liên tục được cải thiện kể từ Python 3.7 (vốn dĩ đã rất tốt) và bao phủ nhiều trường hợp sử dụng khi bạn cần viết lớp. Nhưng chúng có thể không phù hợp trong các tình huống sau:
__init__ tùy chỉnh
__new__ tùy chỉnh
Nhiều mô hình kế thừa khác nhau
Và còn nhiều nữa, như đã thảo luận trong chủ đề Reddit rất hay này. Nếu bạn muốn lý do chi tiết hơn về việc tại sao data class được giới thiệu và vì sao chúng không phải là sự thay thế “thả vào là chạy” cho định nghĩa lớp truyền thống, hãy đọc PEP 557.
Nếu bạn quan tâm đến lập trình hướng đối tượng nói chung, đây là một khóa học để tiếp tục hành trình:
Về bản chất, data class là các cấu trúc “sang chảnh” hơn để lưu trữ và truy xuất dữ liệu hiệu quả hơn. Tuy nhiên, Python có nhiều cấu trúc dữ liệu khác thực hiện nhiệm vụ này theo cách tương tự. Ví dụ, bạn có thể tìm hiểu về counter, defaultdict và namedtuple trong chương cuối của khóa Data Types for Data Science.
Nâng cao kỹ năng về dữ liệu, khám phá cách thao tác và trực quan hóa dữ liệu, đồng thời áp dụng phân tích nâng cao để đưa ra quyết định dựa trên dữ liệu.
Data class là một trong những tính năng của Python mà một khi bạn đã biết đến, bạn sẽ chẳng muốn quay lại cách cũ nữa. Hãy xem lớp thông thường sau:
Với tôi, định nghĩa lớp này rất kém hiệu quả — trong phương thức
__init__, bạn lặp lại mỗi tham số ít nhất ba lần. Nghe có vẻ không nghiêm trọng, nhưng hãy nghĩ tới tần suất bạn viết các lớp trong suốt sự nghiệp với nhiều tham số hơn thế.So sánh với lựa chọn data class cho đoạn mã trên:
Đoạn mã trông khiêm tốn này tốt hơn lớp thông thường rất nhiều lần. Decorator nhỏ
@dataclasssẽ tự động triển khai các phương thức__init__,__repr__,__eq__ở phía sau, vốn sẽ tốn ít nhất 20 dòng nếu viết tay.Ngoài ra, nhiều tính năng khác như toán tử so sánh, sắp xếp đối tượng và tính bất biến đều chỉ cách một dòng để “tự sinh” cho lớp của chúng ta.
Vì vậy, mục tiêu của hướng dẫn này là cho bạn thấy vì sao data class là một trong những điều tuyệt vời nhất xảy đến với Python nếu bạn yêu thích lập trình hướng đối tượng.
Bắt đầu thôi!
Những điều cơ bản về Data Class trong Python
Hãy điểm qua một số khái niệm nền tảng của data class trong Python khiến chúng hữu ích đến vậy.
Một số phương thức được tạo tự động trong data class
Bất chấp mọi tính năng, data class vẫn là các lớp thông thường nhưng cần ít mã hơn nhiều để triển khai cùng chức năng. Đây là lớp
Exercisemột lần nữa:Hiện tại,
Exerciseđã có sẵn các phương thức__repr__và__eq__. Hãy kiểm tra:Biểu diễn đối tượng của
reprphải trả về đoạn mã có thể tái tạo chính nó, và ta thấy điều đó đúng hệt vớiex1.Trong khi đó,
Exerciseđịnh nghĩa theo cách cũ sẽ trông như sau:Trông khá tệ và vô dụng!
Giờ hãy kiểm tra sự tồn tại của
__eq__, tức toán tử bằng nhau:So sánh lớp với chính nó và với một lớp khác có tham số giống hệt nhau phải trả về True:
Và đúng là như vậy! Với các lớp thông thường, viết logic này sẽ rất phiền phức.
Data class yêu cầu gợi ý kiểu
Như bạn có thể thấy, data class yêu cầu gợi ý kiểu khi định nghĩa trường. Thực tế, data class chấp nhận mọi kiểu từ mô-đun
typing. Ví dụ, đây là cách tạo một trường có thể nhận kiểu dữ liệuAny:Tuy nhiên, “cá tính” của Python là dù data class yêu cầu gợi ý kiểu, các kiểu lại không thực sự bị ép buộc.
Chẳng hạn, tạo một thể hiện của lớp
Exercisevới kiểu dữ liệu hoàn toàn sai vẫn chạy không lỗi:Nếu bạn muốn ép kiểu dữ liệu, bạn phải dùng các công cụ kiểm tra kiểu như Mypy.
Data class cho phép giá trị mặc định trong các trường
Cho tới giờ, chúng ta chưa thêm mặc định nào vào lớp. Hãy sửa điều đó:
Lưu ý rằng các trường không mặc định không thể đứng sau các trường mặc định. Ví dụ, đoạn mã dưới đây sẽ báo lỗi:
Trong thực tế, bạn hiếm khi định nghĩa mặc định bằng cú pháp
name: type = value.Thay vào đó, bạn sẽ dùng hàm
field, cho phép kiểm soát nhiều hơn đối với từng định nghĩa trường:Hàm
fieldcó thêm nhiều tham số, chẳng hạn:reprinitcomparedefault_factoryv.v. Chúng ta sẽ bàn tới trong các phần sau.
Có thể tạo data class bằng một hàm
Một ghi chú cuối về cơ bản của data class là định nghĩa của chúng còn có thể ngắn hơn nữa với hàm
make_dataclass:Nhưng bạn sẽ phải đánh đổi tính dễ đọc, nên tôi không khuyến nghị dùng hàm này.
Data Class nâng cao trong Python
Trong phần này, chúng ta sẽ bàn về các tính năng nâng cao của data class mang lại nhiều lợi ích hơn. Một trong số đó là default factory.
Default factory
Để giải thích default factory, hãy tạo một lớp khác tên là
WorkoutSessionnhận hai trường:Bằng cách dùng kiểu
List, ta chỉ định rằngWorkoutSessionnhận một danh sách các thể hiệnExercise.Hiện tại, mỗi thể hiện buổi tập đều cần có bài tập khi khởi tạo. Nhưng điều này không phản ánh cách mọi người tập — trước tiên họ bắt đầu một buổi (có lẽ trong ứng dụng), rồi thêm bài tập khi tập.
Vì vậy, chúng ta cần có thể tạo buổi tập không có bài tập và không có thời lượng. Hãy làm điều này bằng cách thêm danh sách rỗng làm mặc định cho
exercises:Tuy nhiên, ta gặp lỗi — hóa ra data class không cho phép giá trị mặc định có thể thay đổi (mutable).
May mắn là ta có thể sửa bằng cách dùng default factory:
Tham số
default_factorynhận một hàm trả về giá trị khởi tạo cho một trường của data class. Tức là nó có thể nhận bất kỳ hàm tùy ý nào:tupledictsetĐiều này đúng bất kể kết quả của hàm là có thể thay đổi hay không.
Giờ nghĩ kỹ, hầu hết mọi người bắt đầu tập bằng các bài khởi động khá giống nhau cho mọi kiểu buổi tập. Vậy khởi tạo buổi tập mà không có bài tập có thể không đúng với một số người.
Thay vào đó, hãy tạo một hàm trả về ba
Exercisekhởi động:Giờ đây, mỗi khi tạo buổi tập, chúng sẽ đi kèm một vài bài khởi động đã được ghi lại. Phiên bản mới của
WorkoutSessioncó thời lượng mặc định năm phút để phản ánh điều đó.Thêm phương thức vào data class
Vì data class là các lớp thông thường, việc thêm phương thức không thay đổi. Hãy thêm hai phương thức cho data class
WorkoutSessioncủa chúng ta:Sử dụng các phương thức này, giờ ta có thể ghi lại mọi hoạt động mới vào một buổi tập:
Nhưng có một vấn đề:
Khi in buổi tập, biểu diễn mặc định quá dài và khó đọc vì chứa mã để tái tạo đối tượng. Hãy sửa điều đó.
__repr__và__str__trong data classData class tự động triển khai
__repr__nhưng không có__str__. Vì vậy, khi gọi print, lớp sẽ dùng tạm__repr__.Vậy hãy ghi đè hành vi này bằng cách tự định nghĩa
__str__:__repr__vẫn như cũ, nhưng khi gọiprint:Biểu diễn dạng chuỗi của lớp dễ chịu hơn nhiều. Giờ hãy sửa cả
WorkoutSession:Lưu ý: Hãy dùng nút “Giải thích mã” ở cuối đoạn để xem giải thích từng dòng.
Giờ chúng ta đã có đầu ra gọn gàng và dễ đọc.
So sánh trong data class
Với nhiều lớp, việc so sánh các đối tượng theo một logic nào đó là hợp lý. Với các buổi tập, có thể so sánh theo thời lượng, cường độ bài tập hoặc mức tạ.
Trước hết, hãy xem điều gì xảy ra nếu ta thử so sánh hai buổi tập ở trạng thái hiện tại:
Ta nhận
TypeErrorvì data class không triển khai toán tử so sánh. Nhưng có thể khắc phục dễ dàng bằng cách đặt tham sốorderthànhTrue:Lần này phép so sánh hoạt động, nhưng ta đang so sánh cái gì?
Trong data class, phép so sánh được thực hiện theo thứ tự các trường được định nghĩa. Lúc này, các lớp được so sánh dựa trên thời lượng buổi tập vì trường đầu tiên,
exercises, chứa các đối tượng không chuẩn.Ta có thể kiểm tra bằng cách tăng thời lượng buổi tập thứ Tư:
Đúng như dự đoán, chúng ta nhận
False.Nhưng điều gì sẽ xảy ra nếu trường đầu tiên của
Workoutlà kiểu khác, ví dụ một chuỗi? Hãy thử:Dù buổi Thứ Hai kéo dài hơn, phép so sánh cho biết nó nhỏ hơn Thứ Tư. Lý do là “25” đứng trước “27” trong so sánh chuỗi của Python.
Vậy làm sao giữ nguyên thứ tự trường mà vẫn sắp xếp buổi tập theo thời lượng? Rất đơn giản với hàm
field:Bằng cách đặt
comparethànhFalsecho bất kỳ trường nào, ta loại trừ nó khỏi việc so sánh, như kết quả trên cho thấy.Xử lý trường sau khởi tạo (post-init)
Hiện tại, ta đặt thời lượng mặc định là năm phút để tính cho bài khởi động. Tuy nhiên, điều này chỉ hợp lý nếu người dùng bắt đầu bằng khởi động. Nếu họ bắt đầu bằng bài khác thì sao:
Với chỉ một bài tập, tổng thời lượng là năm phút là không hợp lý. Mỗi buổi tập phải ước tính thời lượng động dựa trên số set của mỗi bài. Tức là ta nên làm cho
duration_minutesphụ thuộc vào trườngexercises.Hãy triển khai:
Lần này, ta định nghĩa
duration_minutesvớiinitđặt làFalseđể trì hoãn khởi tạo trường.Sau đó, trong phương thức đặc biệt
__post_init__, ta cập nhật giá trị dựa trên tổng số set của mỗiExercise.Giờ khi khởi tạo
WorkoutSession,duration_minutessẽ tăng động thêm ba phút cho mỗi set trong mỗi bài tập.Nhìn chung, nếu bạn muốn định nghĩa một trường phụ thuộc các trường khác của data class, bạn có thể dùng logic
__post_init__.Tính bất biến trong Data Class
Data class
WorkoutSessioncủa chúng ta gần như sẵn sàng; chỉ còn cần được bảo vệ. Hiện tại, nó có thể bị sửa bừa rất dễ:Chúng ta muốn bảo vệ mọi trường của lớp để chỉ có thể sửa theo cách ta mong muốn. Để làm điều này, decorator
@dataclasscung cấp tham số tiện lợifrozen:Giờ nếu muốn sửa bất kỳ trường nào, ta sẽ nhận lỗi:
Đặt
frozenlàTruesẽ tự động thêm các phương thức__deleteattr__và__setattr__cho mỗi trường để ngăn xóa hoặc cập nhật sau khi khởi tạo. Ngoài ra, người khác cũng không thể thêm trường mới:Chức năng này sẽ tốn hàng chục dòng nếu chúng ta dùng lớp truyền thống.
Tuy nhiên, lưu ý rằng ta không thể làm cho lớp thực sự bất biến. Ví dụ, hãy viết lại
WorkoutSessionvớifrozenlàTrue:Như dự đoán, ta không thể sửa trực tiếp danh sách bài tập:
Tuy nhiên,
exerciseslà một danh sách, hoàn toàn có thể thay đổi, khiến thao tác sau khả thi:# Thay đổi một phần tử trong danh sách
Vì vậy, để tránh thay đổi ngoài ý muốn, khuyến nghị dùng các đối tượng bất biến như tuple cho giá trị trường.
Kế thừa trong data class
Điểm cuối cùng chúng ta sẽ đề cập là thứ tự các trường trong lớp cha và lớp con.
Vì data class là các lớp thông thường, kế thừa hoạt động như bình thường:
Nhưng vì trường cuối trong lớp cha (
ImmutableWorkoutSession) có giá trị mặc định, tất cả trường trong lớp con cũng phải có giá trị mặc định.Ví dụ, điều này là không được phép:
Nhược điểm của Data Class và Tài nguyên tham khảo
Data class đã liên tục được cải thiện kể từ Python 3.7 (vốn dĩ đã rất tốt) và bao phủ nhiều trường hợp sử dụng khi bạn cần viết lớp. Nhưng chúng có thể không phù hợp trong các tình huống sau:
__init__tùy chỉnh__new__tùy chỉnhVà còn nhiều nữa, như đã thảo luận trong chủ đề Reddit rất hay này. Nếu bạn muốn lý do chi tiết hơn về việc tại sao data class được giới thiệu và vì sao chúng không phải là sự thay thế “thả vào là chạy” cho định nghĩa lớp truyền thống, hãy đọc PEP 557.
Nếu bạn quan tâm đến lập trình hướng đối tượng nói chung, đây là một khóa học để tiếp tục hành trình:
Về bản chất, data class là các cấu trúc “sang chảnh” hơn để lưu trữ và truy xuất dữ liệu hiệu quả hơn. Tuy nhiên, Python có nhiều cấu trúc dữ liệu khác thực hiện nhiệm vụ này theo cách tương tự. Ví dụ, bạn có thể tìm hiểu về counter,
defaultdictvànamedtupletrong chương cuối của khóa Data Types for Data Science.