Học bằng cách đọc theo thứ tự

Đ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 ManagerEngineer 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.

Ba class override calculate_salary
Employee(cha)base_salary(không đổi)= 300kManager(con override)base +team x 50k= 1,2MEngineer(con override)base +skill x 20k= 380k
Lớp cha 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

Định nghĩa Employee / Manager / Engineer và xác nhận mỗi calculate_salary trả kết quả khác nhau.

① Xây ba class theo ví dụ trên (ManagerEngineer cần gọi super().__init__(name, base_salary) để giao việc init cho cha).

② Tạo Employee("Minh", 300000), Manager("Linh", 800000, 8), và Engineer("Hùng", 300000, 4), sau đó print kết quả calculate_salary() của từng cái.

(Khi chạy đúng, phần giải thích sẽ hiện ra.)

Python Editor

Chạy code để xem đầu ra
emp.calculate_salary() hành xử ra sao trong vòng lặp
emp.calculate_salary()(cùng dòng)emp = minh(Employee)-> 300kemp = linh(Manager)-> 1,2Memp = hung(Engineer)-> 380kEmployeeManagerEngineer
Thân vòng lặp chỉ là emp.calculate_salary() trên một dòng duy nhất, nhưng method khác nhau được chọn tự động tùy class của 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
PayrollSystem không quan tâm bên trong là gì
PayrollSystem
  • employees = [...] (type trộn lẫn)
  • for emp in self.employees: duyệt qua chúng
  • Chỉ cần gọi emp.calculate_salary()
Employee (mặc định)
  • trả base_salary
Manager
  • base + team * 50k
Engineer
  • base + skill * 20k
Dù trộn ba class khác nhau trong list, PayrollSystem chỉ gọi cùng tên method và phép tính đúng chạy theo type.

Tái sử dụng ba class từ Thực hành 1 để xây PayrollSystem tính tổng lương (console giữ trạng thái, nên các định nghĩa class trước vẫn còn).

① Định nghĩa class PayrollSystem: với __init__ khởi tạo self.employees = []. Triển khai add(self, employee)total(self) (để total duyệt vòng for cộng từng emp.calculate_salary()).

② Tạo payroll = PayrollSystem(), sau đó add ba nhân viên: Employee("Minh", 300000) / Manager("Linh", 800000, 8) / Engineer("Hùng", 300000, 4).

③ Duyệt payroll.employees in emp.nameemp.calculate_salary() từng dòng, sau đó in payroll.total() với tiền tố "tổng:".

Python Editor

Chạy code để xem đầu ra

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
Rẽ nhánh theo type vs đa hình
XẤU: rẽ theo typeif type ==x N lầnmỗi type mới= nhánh mớiTỐT: đa hìnhemp.calculate()type mới =chỉ thêm classlankhép kín
Code rẽ nhánh theo type chất 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, CatDog 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

Thiết kế đa hình vs thiết kế rẽ nhánh theo type
Thiết kế đa hình
  • 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à đủ
Thiết kế rẽ nhánh theo type
  • 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
Cùng yêu cầu, hai thiết kế — nhưng bao nhiêu code mà bên gọi viếtcác thay đổi lan đi đâu có thể khác nhau rất nhiều.
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Mô tả nào chính xác nhất về đa hình?

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?