← Lập trình Python nâng cao

Bài 2 · Nâng cao · 26 phút· Cập nhật 11/06/2026

Kế thừa, đa hình & lớp trừu tượng

Biên soạn bởi Nguyễn Anh Tuấn

Bốn trụ cột OOP trong Python: kế thừa (super, override), đa hình & duck typing, lớp trừu tượng (abc) và đóng gói - cùng MRO, Enum.

Bài trước nhìn OOP qua mô hình dữ liệu (dunder, property, dataclass). Bài này là bốn trụ cột OOP “cổ điển”. Đầu tiên: kế thừa - lớp con dùng lại lớp cha rồi ghi đè (override) phần khác; super() gọi ngược lên cha để khỏi chép code.

ke_thua.py

class DongVat:
    def __init__(self, ten):
        self.ten = ten
    def keu(self):
        return "..."
    def gioi_thieu(self):
        return f"{self.ten} keu {self.keu()}"

class Cho(DongVat):
    def keu(self):                 # GHI DE (override)
        return "Gau gau"

class Meo(DongVat):
    def __init__(self, ten, mau):
        super().__init__(ten)      # goi __init__ cua cha, khoi chep
        self.mau = mau
    def keu(self):
        return "Meo meo"

print(Cho("Lu").gioi_thieu())      # Lu keu Gau gau
print(Meo("Mun", "den").gioi_thieu())

Kết quả khi chạy

Lu keu Gau gau
Mun keu Meo meo
  • Lớp con kế thừa thuộc tính/phương thức của cha; chỉ viết phần KHÁC.
  • Override: định nghĩa lại một phương thức ở lớp con để đổi hành vi.
  • super() gọi phiên bản của lớp cha - đặc biệt cần trong __init__ để cha khởi tạo phần của nó.

Đừng lạm dụng kế thừa sâu

Kế thừa nhiều tầng dễ rối và khó sửa. Nguyên tắc thực dụng: ưu tiên GHÉP (composition) - “có một” (has-a) - hơn kế thừa “là một” (is-a) khi không thật sự cần.

Đa hình (polymorphism): cùng một lời gọi (.dien_tich()) chạy đúng cài đặt của từng loại. Python theo duck typing: “nếu nó kêu như vịt thì coi như vịt” - chỉ cần object có phương thức cần dùng, KHÔNG bắt buộc cùng lớp cha.

da_hinh.py

class HinhVuong:
    def __init__(self, c): self.c = c
    def dien_tich(self): return self.c * self.c

class HinhTron:
    def __init__(self, r): self.r = r
    def dien_tich(self): return 3.14159 * self.r ** 2

# Hai lop KHONG chung cha - chi can deu co .dien_tich()
hinh = [HinhVuong(2), HinhTron(3)]
print(sum(h.dien_tich() for h in hinh))   # goi dung phuong thuc cua tung loai

Kết quả khi chạy

32.27431
  • Đa hình = một lời gọi, nhiều hành vi tuỳ loại object.
  • Duck typing: Python quan tâm object CÓ phương thức gì, không phải nó thuộc lớp nào.
  • Thêm loại hình mới chỉ cần có .dien_tich() - vòng lặp xử lý không phải sửa.

Python cho phép đa kế thừa. Khi đó “gọi phương thức nào / super() đi đâu” được quyết bởi MRO (Method Resolution Order) - một thứ tự tính bằng thuật toán C3, xem qua __mro__.

mro.py - kim cuong

class A:
    def chao(self): return "A"
class B(A):
    def chao(self): return "B -> " + super().chao()
class C(A):
    def chao(self): return "C -> " + super().chao()
class D(B, C):
    pass

print(D().chao())                          # B -> C -> A
print([k.__name__ for k in D.__mro__])     # ['D', 'B', 'C', 'A', 'object']

Kết quả khi chạy

B -> C -> A
['D', 'B', 'C', 'A', 'object']

Trung thực: super() KHÔNG phải “cha trực tiếp”

Trong B, super().chao() chạy C chứ không phải A - vì super() đi theo MRO, không phải cây kế thừa “thẳng”. Nhờ vậy mỗi lớp chạy đúng MỘT lần (giải quyết “diamond problem”). Đa kế thừa mạnh nhưng dễ rối - thường chỉ dùng cho mixin (lớp nhỏ thêm một năng lực).

Khi muốn ép mọi lớp con phải cài một số phương thức, dùng lớp trừu tượng: kế thừa abc.ABC và đánh dấu @abstractmethod. Lớp còn thiếu method thì không tạo được object.

abc_demo.py

from abc import ABC, abstractmethod

class Hinh(ABC):
    @abstractmethod
    def dien_tich(self): ...        # chi la "hop dong", khong cai dat

class Vuong(Hinh):
    def __init__(self, c): self.c = c
    def dien_tich(self): return self.c * self.c

print(Vuong(3).dien_tich())         # 9
# Hinh()  -> TypeError: Can't instantiate abstract class Hinh
# class Thieu(Hinh): pass; Thieu() -> cung loi vi chua cai dien_tich()

Kết quả khi chạy

9
  • abc.ABC + @abstractmethod = “hợp đồng”: lớp con BẮT BUỘC cài đủ.
  • Không thể tạo object từ lớp trừu tượng (hay lớp con còn thiếu method) → lỗi ngay.
  • Khác duck typing (không ràng buộc) và Protocol (structural, không cần kế thừa).

Phương thức thường nhận self (một object cụ thể). @classmethod nhận cls (chính class) - hợp làm constructor thay thế. @staticmethod không nhận gì cả - chỉ là hàm tiện ích gom vào class cho gọn.

class_static.py

class Ngay:
    def __init__(self, d, m, y):
        self.d, self.m, self.y = d, m, y

    @classmethod
    def tu_chuoi(cls, s):              # constructor thay the; cls = chinh class
        d, m, y = map(int, s.split("/"))
        return cls(d, m, y)            # dung cls -> ke thua van dung dung lop con

    @staticmethod
    def la_nam_nhuan(y):              # khong self/cls - chi la tien ich
        return y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)

ng = Ngay.tu_chuoi("2/9/1945")
print(ng.y, Ngay.la_nam_nhuan(2024), Ngay.la_nam_nhuan(1900))

Kết quả khi chạy

1945 True False
  • instance method (self): thao tác trên một đối tượng cụ thể.
  • @classmethod (cls): dùng class - lý tưởng cho constructor thay thế, kế thừa đúng.
  • @staticmethod: không self/cls - hàm độc lập nhưng liên quan, đặt trong class cho gọn.

Python không có “private” thật. Quy ước: một gạch dưới _x nghĩa là “nội bộ, đừng đụng từ ngoài”. Hai gạch dưới __x kích hoạt name mangling - Python đổi tên thành _TênLớp__x để tránh trùng khi kế thừa.

dong_goi.py

class TaiKhoan:
    def __init__(self, so_du):
        self._chu = "an"          # _x: QUY UOC noi bo (Python khong chan)
        self.__so_du = so_du      # __x: doi ten -> _TaiKhoan__so_du

    @property
    def so_du(self):              # kiem soat truy cap qua property
        return self.__so_du

    def nap(self, tien):
        if tien <= 0: raise ValueError("phai > 0")
        self.__so_du += tien

t = TaiKhoan(100)
t.nap(50); print(t.so_du)         # 150
# t.__so_du            -> AttributeError (da bi doi ten)
print(t._TaiKhoan__so_du)         # 150  (van truy cap duoc -> khong phai bao mat)

Kết quả khi chạy

150
150

“We are all consenting adults”

Gạch dưới chỉ là quy ước & chống trùng tên, KHÔNG phải cơ chế bảo mật - đừng dựa vào nó để giấu bí mật. Muốn KIỂM SOÁT đọc/ghi (kiểm tra giá trị, chỉ-đọc…), hãy dùng @property như ở bài “OOP & mô hình dữ liệu”.

Khi một biến chỉ nhận vài giá trị cố định (trạng thái, màu, hướng…), đừng dùng chuỗi "cho"/"xong" hay số rời - dễ gõ sai mà không báo lỗi. Dùng Enum: gom thành nhóm có tên, an toàn và rõ nghĩa.

enum_demo.py

from enum import Enum, auto

class TrangThai(Enum):
    CHO = auto()          # 1
    DANG_LAM = auto()     # 2
    XONG = auto()         # 3

print(TrangThai.XONG)                     # TrangThai.XONG
print(TrangThai.XONG.name, TrangThai.XONG.value)   # XONG 3

def mo_ta(t):
    if t is TrangThai.XONG:               # so sanh bang 'is' -> ro & an toan
        return "Hoan tat"
    return "Chua xong"

print(mo_ta(TrangThai.XONG))              # Hoan tat
# TrangThai.XONGG  -> AttributeError ngay (go sai bao loi luon)

Kết quả khi chạy

TrangThai.XONG
XONG 3
Hoan tat
  • Enum gom các giá trị cố định thành nhóm có tên → an toàn hơn chuỗi/số rời.
  • Gõ sai tên thành viên → lỗi NGAY (chuỗi gõ sai thì im lặng chạy sai).
  • So sánh bằng is; auto() tự đánh số; IntEnum khi cần so sánh như số.

Tiếp theo

Bạn đã đủ bốn trụ cột OOP (kế thừa, đa hình, trừu tượng, đóng gói) cộng Enum. Bài sau quay lại “cách Python lặp”: Iterator & generator - bản chất của vòng for.

Câu hỏi thường gặp

Dùng KẾ THỪA khi thật sự “is-a” (Mèo LÀ Động vật) và muốn chia sẻ phần cài đặt. Dùng DUCK TYPING (hoặc Protocol ở bài type hints) khi chỉ cần “object có phương thức X” - không quan tâm dòng dõi. Nguyên tắc chung: ưu tiên GHÉP (composition) hơn kế thừa sâu; kế thừa nhiều tầng dễ rối.

Theo MRO (Method Resolution Order) - thứ tự Python tính bằng thuật toán C3, KHÔNG phải “cha viết đầu tiên”. Xem bằng Class.__mro__. super() chuyển tiếp tới lớp KẾ TIẾP trong MRO, nhờ vậy mỗi lớp trong cây chạy đúng một lần (giải quyết “diamond problem”).

ABC (abc.ABC + @abstractmethod) RÀNG BUỘC lúc tạo object: lớp con quên viết abstractmethod thì KHÔNG tạo được - và phải kế thừa tường minh. Protocol (bài type hints) là “structural”: khớp theo hình dạng, không cần kế thừa, kiểm lúc gõ kiểu. Duck typing thuần thì không ràng buộc gì, lỗi chỉ lộ lúc chạy.

Bài Type hints & mypy →

classmethod nhận cls (chính class) → hợp làm “constructor thay thế” (vd Ngay.tu_chuoi("2/9/1945")) và chạy đúng cả khi bị kế thừa. staticmethod KHÔNG nhận self/cls → chỉ là một hàm tiện ích gom vào trong class cho gọn về mặt tổ chức. Còn instance method thường nhận self (thao tác trên một đối tượng cụ thể).

Không. _x là QUY ƯỚC (“nội bộ, đừng đụng từ ngoài”), Python không chặn. __x kích hoạt “name mangling”: đổi tên thành _TênLớp__x để tránh trùng tên khi kế thừa - vẫn truy cập được qua tên đã đổi. Triết lý Python: “we are all consenting adults”. Muốn KIỂM SOÁT truy cập thì dùng @property (bài “OOP & mô hình dữ liệu”).

Enum gom các giá trị liên quan thành một nhóm có TÊN, an toàn hơn: gõ sai tên → lỗi ngay (chứ chuỗi "xong" gõ thành "xonng" thì im lặng sai), so sánh bằng is rất rõ nghĩa, và tự liệt kê được. Dùng IntEnum nếu cần so sánh như số; auto() để tự đánh số.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Trong cây kim cương class D(B, C), trong B câu super().chao() sẽ gọi tới đâu?

  1. 1

    Kế thừa & super()

    Viết NhanVien(ten, luong) với tinh_luong(); QuanLy kế thừa, override tinh_luong() = lương + thưởng, gọi super().tinh_luong().

    Hoàn thành khi: QuanLy dùng lại logic của NhanVien qua super(), chỉ thêm phần thưởng - không chép lại.

  2. 2

    Đa hình không if-theo-loại

    Tạo list gồm HinhVuong, HinhTron, HinhChuNhat (mỗi lớp có dien_tich()). Tính TỔNG diện tích bằng MỘT vòng lặp, không dùng if/isinstance theo loại.

    Hoàn thành khi: sum(h.dien_tich() for h in hinh) chạy đúng nhờ đa hình - thêm hình mới không phải sửa vòng lặp.

  3. 3

    Đọc MRO

    Dựng kim cương A; B(A); C(A); D(B, C). In D.__mro__ và giải thích vì sao thứ tự là DBCAobject.

    Hoàn thành khi: Giải thích được C3/MRO và vì sao super() trong B lại dẫn sang C (không phải A).

  4. 4

    Lớp trừu tượng (ABC)

    Định nghĩa Kho(ABC) với @abstractmethod luu(x)doc(); viết KhoBoNho cài đủ; rồi thử một lớp THIẾU doc() → tạo object xem lỗi gì.

    Hoàn thành khi: KhoBoNho chạy được; lớp thiếu method bị TypeError “Can’t instantiate abstract class…”.

  5. 5

    classmethod & staticmethod

    Thêm cho lớp Ngay: classmethod hom_nay() (dùng cls) và staticmethod la_nam_nhuan(y).

    Hoàn thành khi: Ngay.hom_nay() trả về một Ngay; Ngay.la_nam_nhuan(2024)True, (1900)False.

  6. 6

    Đổi chuỗi sang Enum

    Lấy một biến trạng thái dạng chuỗi ("cho"/"dang_lam"/"xong"), đổi sang Enum TrangThai; so sánh bằng is; thử gõ sai tên thành viên xem lỗi.

    Hoàn thành khi: So sánh bằng is rõ nghĩa; truy cập thành viên không tồn tại → AttributeError ngay (an toàn hơn chuỗi).