Câu 1Mô tả nào chính xác nhất về đa hình?
Đa hình — Cùng tên method, hành vi khác theo type
Học đa hình trong Python. Override method của lớp cha trong subclass để bên gọi không cần nghĩ về type, và thay các nhánh if type(...) bằng OOP gọn — kèm sơ đồ.
Lần trước ta đã bao quát đa kế thừa và MRO. Để khép lại loạt OOP, bài này bao quát trụ cột thứ ba — đa hình (polymorphism).
Đa hình là gì?
Đa hình là ý tưởng rằng cùng một interface (tên method) có thể làm những việc khác nhau tùy type. Giả sử bạn muốn một thao tác «tính lương» duy nhất, nhưng bạn muốn nhân viên, quản lý, và kỹ sư dùng các công thức khác nhau.
Định nghĩa calculate_salary ở lớp cha Employee, và để các subclass Manager và Engineer override với công thức riêng. Bây giờ code gọi chỉ cần viết employee.calculate_salary() mà không quan tâm class nào, và phép tính đúng sẽ chạy.
Employee định nghĩa calculate_salary. Các subclass Manager / Engineer override nó với công thức riêng. Cùng tên method, kết quả khác nhau theo class.Xây trên lớp cha, đổi công thức ở từng lớp con
Hãy xây thực sự ví dụ tiền lương. Employee là cha, Manager (cộng theo kích thước team) và Engineer (cộng theo skill level) là con. Điều then chốt là cả ba đều định nghĩa method cùng tên calculate_salary.
class Employee:
def __init__(self, name, base_salary):
self.name = name
self.base_salary = base_salary
def calculate_salary(self): # mặc định = chỉ lương cơ bản
return self.base_salary
class Manager(Employee):
def __init__(self, name, base_salary, team_size):
super().__init__(name, base_salary)
self.team_size = team_size
def calculate_salary(self): # cộng theo kích thước team
return self.base_salary + self.team_size * 50000
class Engineer(Employee):
def __init__(self, name, base_salary, skill_level):
super().__init__(name, base_salary)
self.skill_level = skill_level
def calculate_salary(self): # cộng theo skill level
return self.base_salary + self.skill_level * 20000
emp — đó là sức mạnh của đa hình.Gói trong list — Vòng lặp không quan tâm type
Đa hình thực sự thể hiện sức mạnh khi bạn đổ các object có type khác nhau vào cùng một list và xử lý tất cả cùng lúc. Bọc tính lương trong class PayrollSystem và thân vòng lặp gọn lại còn một dòng.
class PayrollSystem:
def __init__(self):
self.employees = []
def add(self, employee):
self.employees.append(employee)
def total(self):
result = 0
for emp in self.employees: # type có thể khác nhau
result += emp.calculate_salary() # cùng tên vẫn ổn
return result
payroll = PayrollSystem()
payroll.add(Employee("Minh", 300000))
payroll.add(Manager("Linh", 800000, 8))
payroll.add(Engineer("Hùng", 300000, 4))
print(payroll.total()) # 1880000
employees = [...](type trộn lẫn)for emp in self.employees:duyệt qua chúng- Chỉ cần gọi emp.calculate_salary()
- trả base_salary
- base + team * 50k
- base + skill * 20k
PayrollSystem chỉ gọi cùng tên method và phép tính đúng chạy theo type.Trông thế nào nếu không có đa hình
Thử làm điều tương tự mà không có đa hình và bạn sẽ phải viết các nhánh if type(emp) == ...:. Nó chạy được, nhưng mỗi vai trò mới cần thêm một nhánh if — và lúc nào bạn quên một, là có bug.
# (XẤU) không đa hình (rẽ nhánh theo type)
def calc(emp):
if type(emp) is Manager:
return emp.base_salary + emp.team_size * 50000
elif type(emp) is Engineer:
return emp.base_salary + emp.skill_level * 20000
else:
return emp.base_salary
# (TỐT) có đa hình (đẩy logic vào class)
def calc(emp):
return emp.calculate_salary() # một dòng
if type lên bên gọi và phình ra với mỗi type mới. Đa hình giữ bên gọi ở một dòng — thêm type mới chỉ là thêm một class.«Bên gọi không quan tâm type» — đó là khẩu hiệu
Một bài kiểm tra đáng tin cậy xem thiết kế của bạn có đa hình không: code bên gọi có chất đống if type(...) hoặc if isinstance(...) không? Nếu có, refactor chuẩn là đẩy nhánh đó vào method override trên class. «Thêm class» và «thêm if» thường là đánh đổi.
Duck typing — Chỉ cần cùng tên method
Đa hình của Python còn có một vị mềm hơn: duck typing. Câu nói «nếu nó đi như vịt và kêu như vịt thì nó là vịt» trở thành «nếu một class có method đúng, không quan trọng nó là class nào».
Trong ví dụ dưới, Cat và Dog không hề chia sẻ một lớp cha Animal chung — nhưng chừng nào cả hai đều có method speak(), cùng một hàm xử lý cả hai. Python ưu tiên «có method đó không?» hơn đồ thị kế thừa.
class Cat:
def speak(self):
return "Meo"
class Dog:
def speak(self):
return "Gâu"
def shout(animal): # type không bị ép
print(animal.speak())
shout(Cat()) # Meo
shout(Dog()) # Gâu
Các ngôn ngữ như Java hay C# yêu cầu một lớp cha chung để đa hình hoạt động. Python hài lòng miễn là method có mặt lúc gọi. Điều đó cho bạn linh hoạt, nhưng «đảm bảo tên method có cùng ý nghĩa qua các class» trở thành trách nhiệm của bên gọi — nhớ điều đó.
Hai thiết kế trên một slide
- Code bên gọi — một dòng
emp.calculate_salary() - Thêm type mới — chỉ cần viết class mới có
calculate_salary - Bán kính tác động — gói gọn trong class
- Khả năng đọc — đọc «cùng tên, khác theo type» là đủ
- Code bên gọi — chất đống
if type(emp) is ... - Thêm type mới — phải xem lại tất cả nhánh
- Bán kính tác động — lan ra bên gọi
- Khả năng đọc — đọc lại logic nhánh mỗi lần
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Đa hình có xu hướng loại bỏ cấu trúc nào khỏi code bên gọi?
Câu 3Mô tả nào tốt nhất về duck typing của Python?