M14 — Lập trình hướng đối tượng (OOP)

← 13 - Làm việc với File | Tiếp theo → 15 - Môi trường ảo và pip

Mục tiêu module

  • Hiểu class (lớp) và object (đối tượng)
  • Viết __init__, thuộc tính, phương thức
  • Nắm 4 trụ cột OOP ở mức nhập môn
  • Biết @dataclass — cách viết class gọn hiện đại

Thời lượng: ~9 giờ | Tuần: 22–23

Tuần 22: Mục 1–7 | Tuần 23: Mục 8–13


PHẦN 1 — TUẦN 22

1. Vì sao cần OOP?

1.1. Vấn đề với cách viết hiện tại

Đến giờ bạn biểu diễn một khách hàng bằng dict:

kh = {"ma": "KH001", "ten": "Nguyen Van A", "cong_no": 5_000_000}

Cách này có 4 vấn đề:

Vấn đềMinh họa
Không kiểm soát dữ liệukh["cong_no"] = -999999 — không ai ngăn được
Dễ gõ sai khóakh["cog_no"] → KeyError chỉ khi chạy tới
Không biết có khóa nàoPhải nhớ hoặc print ra xem
Dữ liệu và hành vi tách rờitinh_lai_qua_han(kh) — hàm nằm rời rạc, không gắn với dữ liệu

Class giải quyết cả 4: gói dữ liệu (thuộc tính) và hành vi (phương thức) vào một khối, có kiểm soát.

1.2. So sánh với thứ bạn đã biết

Khái niệm OOPTương tự trong database
Class (lớp)Định nghĩa bảng (CREATE TABLE) — bản thiết kế
Object (đối tượng)Một dòng dữ liệu cụ thể trong bảng
Attribute (thuộc tính)Một cột
Method (phương thức)Stored procedure gắn với bảng đó
class KhachHang:          # Bản thiết kế, giống CREATE TABLE
    ...
 
kh1 = KhachHang(...)      # Một đối tượng, giống một row
kh2 = KhachHang(...)      # Một đối tượng khác

2. Class đầu tiên

class KhachHang:
    """Đại diện một khách hàng."""
 
    def __init__(self, ma, ten, cong_no=0):
        """Khởi tạo khách hàng mới."""
        self.ma = ma
        self.ten = ten
        self.cong_no = cong_no
 
 
# Tạo đối tượng (instantiate)
kh1 = KhachHang("KH001", "Nguyen Van A", 5_000_000)
kh2 = KhachHang("KH002", "Tran Thi B")
 
print(kh1.ma)          # KH001
print(kh1.cong_no)     # 5000000
print(kh2.cong_no)     # 0  — dùng giá trị mặc định

2.1. Mổ xẻ

Thành phầnVai trò
class KhachHang:Định nghĩa lớp. Tên class dùng PascalCase theo PEP 8
__init__Constructor (hàm khởi tạo) — chạy tự động khi tạo đối tượng
selfTham chiếu tới chính đối tượng đang được thao tác
self.ma = maGán giá trị vào thuộc tính của đối tượng

2.2. self là gì?

self là tham số đầu tiên bắt buộc của mọi method. Python tự truyền vào — bạn không truyền thủ công.

kh1 = KhachHang("KH001", "Nguyen Van A")
# Python thực chất gọi: KhachHang.__init__(kh1, "KH001", "Nguyen Van A")
#                                          ^^^ self
kh1.ma      # Đọc thuộc tính của riêng kh1
kh2.ma      # Đọc thuộc tính của riêng kh2 — độc lập hoàn toàn

Quên self là lỗi phổ biến nhất

class A:
    def __init__(ma):        # ❌ Thiếu self
        self.ma = ma
 
class B:
    def __init__(self, ma):
        ma = ma              # ❌ Gán biến cục bộ, không phải thuộc tính
 
class C:
    def __init__(self, ma):
        self.ma = ma         # ✅ Đúng

2.3. __init__ không phải “constructor” theo nghĩa đầy đủ

Tên có 2 dấu gạch dưới hai bên gọi là dunder method (double underscore). Python tự gọi chúng trong các tình huống nhất định. Xem thêm mục 7.


3. Method — Phương thức

from datetime import date
 
 
class KhachHang:
    """Đại diện một khách hàng có công nợ."""
 
    def __init__(self, ma, ten, cong_no=0, han_thanh_toan=None):
        self.ma = ma
        self.ten = ten
        self.cong_no = cong_no
        self.han_thanh_toan = han_thanh_toan
        self.lich_su = []
 
    def thanh_toan(self, so_tien):
        """Ghi nhận một khoản thanh toán."""
        if so_tien <= 0:
            raise ValueError("So tien thanh toan phai lon hon 0")
        if so_tien > self.cong_no:
            raise ValueError(
                f"So tien thanh toan ({so_tien:,}) vuot cong no ({self.cong_no:,})"
            )
        self.cong_no -= so_tien
        self.lich_su.append((date.today(), "Thanh toan", so_tien))
        return self.cong_no
 
    def phat_sinh_no(self, so_tien):
        """Ghi nhận một khoản nợ mới."""
        if so_tien <= 0:
            raise ValueError("So tien phai lon hon 0")
        self.cong_no += so_tien
        self.lich_su.append((date.today(), "Phat sinh", so_tien))
        return self.cong_no
 
    def qua_han(self):
        """Kiểm tra khách hàng có nợ quá hạn không."""
        if self.han_thanh_toan is None or self.cong_no == 0:
            return False
        return date.today() > self.han_thanh_toan
 
    def so_ngay_qua_han(self):
        """Trả về số ngày quá hạn, 0 nếu chưa quá hạn."""
        if not self.qua_han():
            return 0
        return (date.today() - self.han_thanh_toan).days
 
    def in_thong_tin(self):
        """In thông tin khách hàng ra màn hình."""
        print(f"Ma        : {self.ma}")
        print(f"Ten       : {self.ten}")
        print(f"Cong no   : {self.cong_no:,.0f} VND")
        if self.qua_han():
            print(f"Trang thai: QUA HAN {self.so_ngay_qua_han()} ngay")
        else:
            print("Trang thai: Binh thuong")
 
 
kh = KhachHang("KH001", "Nguyen Van A", 10_000_000, date(2026, 7, 1))
kh.in_thong_tin()
print()
 
kh.thanh_toan(3_000_000)
print(f"Sau thanh toan: {kh.cong_no:,}")
 
kh.phat_sinh_no(5_000_000)
print(f"Sau phat sinh : {kh.cong_no:,}")
print(f"Lich su       : {kh.lich_su}")

3.1. Method vs Function

Function (hàm)Method (phương thức)
Định nghĩa ởNgoài classTrong class
Tham số đầuBất kỳself
Cách gọiham(doi_tuong)doi_tuong.method()
# Function
def thanh_toan(kh_dict, so_tien):
    kh_dict["cong_no"] -= so_tien
 
thanh_toan(kh, 1000)
 
# Method
kh.thanh_toan(1000)      # Gọn hơn, dữ liệu và hành vi gắn liền

4. Class attribute vs Instance attribute

class KhachHang:
    # CLASS ATTRIBUTE — chung cho MỌI đối tượng
    so_luong_khach = 0
    HAN_MUC_MAC_DINH = 50_000_000
 
    def __init__(self, ma, ten):
        # INSTANCE ATTRIBUTE — riêng của TỪNG đối tượng
        self.ma = ma
        self.ten = ten
        KhachHang.so_luong_khach += 1
 
 
kh1 = KhachHang("KH001", "A")
kh2 = KhachHang("KH002", "B")
kh3 = KhachHang("KH003", "C")
 
print(KhachHang.so_luong_khach)     # 3
print(kh1.so_luong_khach)           # 3  — mọi đối tượng đều thấy
print(kh1.ma, kh2.ma)               # KH001 KH002  — riêng biệt

Bẫy mutable class attribute

class Gio:
    mat_hang = []                 # ❌ Chia sẻ giữa MỌI đối tượng!
 
    def them(self, x):
        self.mat_hang.append(x)
 
g1 = Gio()
g2 = Gio()
g1.them("Ban")
print(g2.mat_hang)                # ['Ban']  <- g2 cũng có!
class Gio:
    def __init__(self):
        self.mat_hang = []        # ✅ Mỗi đối tượng một list riêng

Đây là biến thể của bẫy mutable default đã học ở 10 - Hàm (Function).


5. Encapsulation — Đóng gói

Encapsulation (đóng gói) nghĩa là ẩn chi tiết bên trong, chỉ cho phép truy cập qua giao diện được kiểm soát.

5.1. Quy ước dấu gạch dưới

Python không có từ khóa private/public như C#/Java. Thay vào đó là quy ước:

Cách viếtÝ nghĩaPython có chặn không?
self.tenPublic — dùng tự do—
self._so_du”Nội bộ, đừng đụng vào”❌ Không chặn, chỉ là quy ước
self.__ma_bi_matName mangling — khó truy cập từ ngoài⚠️ Chỉ đổi tên, vẫn truy cập được
class TaiKhoan:
    def __init__(self, so_du):
        self._so_du = so_du          # Quy ước: nội bộ
 
tk = TaiKhoan(1000)
print(tk._so_du)      # 1000 — vẫn đọc được, Python không chặn

Triết lý Python

“We’re all consenting adults here” — chúng ta đều là người trưởng thành. Python tin lập trình viên sẽ tôn trọng quy ước thay vì dựng rào chắn kỹ thuật.

5.2. @property — Thuộc tính có kiểm soát

class TaiKhoan:
    """Tài khoản ngân hàng với số dư được kiểm soát."""
 
    def __init__(self, chu_tk, so_du=0):
        self.chu_tk = chu_tk
        self._so_du = 0
        self.so_du = so_du            # Đi qua setter để được kiểm tra
 
    @property
    def so_du(self):
        """Số dư hiện tại (chỉ đọc qua property)."""
        return self._so_du
 
    @so_du.setter
    def so_du(self, gia_tri):
        """Gán số dư, không cho phép giá trị âm."""
        if gia_tri < 0:
            raise ValueError("So du khong the am")
        self._so_du = gia_tri
 
    @property
    def so_du_dinh_dang(self):
        """Số dư dạng chuỗi định dạng (thuộc tính tính toán, chỉ đọc)."""
        return f"{self._so_du:,.0f} VND"
 
    def nap(self, so_tien):
        """Nạp tiền vào tài khoản."""
        if so_tien <= 0:
            raise ValueError("So tien nap phai lon hon 0")
        self.so_du = self._so_du + so_tien
        return self._so_du
 
    def rut(self, so_tien):
        """Rút tiền khỏi tài khoản."""
        if so_tien <= 0:
            raise ValueError("So tien rut phai lon hon 0")
        if so_tien > self._so_du:
            raise ValueError(
                f"So du khong du. Hien co {self._so_du:,.0f}, "
                f"yeu cau rut {so_tien:,.0f}"
            )
        self.so_du = self._so_du - so_tien
        return self._so_du
 
 
tk = TaiKhoan("Nguyen Van A", 1_000_000)
print(tk.so_du)                  # 1000000  — gọi như thuộc tính, không có ()
print(tk.so_du_dinh_dang)        # 1.000.000 VND
 
tk.nap(500_000)
print(tk.so_du_dinh_dang)        # 1.500.000 VND
 
tk.so_du = -100                  # ❌ ValueError: So du khong the am
tk.rut(999_999_999)              # ❌ ValueError: So du khong du...

@property cho phép giữ cú pháp đơn giản (tk.so_du) mà vẫn chạy code kiểm tra phía sau.


6. Inheritance — Kế thừa

Inheritance cho phép một class thừa hưởng thuộc tính và method của class khác.

class ChungTu:
    """Class cơ sở cho mọi loại chứng từ."""
 
    def __init__(self, so, ngay, so_tien, dien_giai=""):
        self.so = so
        self.ngay = ngay
        self.so_tien = so_tien
        self.dien_giai = dien_giai
        self.da_duyet = False
 
    def duyet(self):
        """Duyệt chứng từ."""
        self.da_duyet = True
 
    def mo_ta(self):
        """Mô tả chứng từ — class con sẽ ghi đè."""
        return f"Chung tu {self.so}"
 
    def in_chung_tu(self):
        print("=" * 50)
        print(self.mo_ta().center(50))
        print("=" * 50)
        print(f"So        : {self.so}")
        print(f"Ngay      : {self.ngay}")
        print(f"So tien   : {self.so_tien:,.0f} VND")
        print(f"Dien giai : {self.dien_giai}")
        print(f"Trang thai: {'Da duyet' if self.da_duyet else 'Chua duyet'}")
        print("=" * 50)
 
 
class PhieuThu(ChungTu):
    """Phiếu thu tiền mặt — kế thừa ChungTu."""
 
    def __init__(self, so, ngay, so_tien, nguoi_nop, dien_giai=""):
        super().__init__(so, ngay, so_tien, dien_giai)   # Gọi __init__ của cha
        self.nguoi_nop = nguoi_nop                       # Thuộc tính riêng
 
    def mo_ta(self):
        """Ghi đè method của class cha."""
        return "PHIEU THU"
 
    def in_chung_tu(self):
        """Mở rộng method của cha."""
        super().in_chung_tu()                            # Gọi bản của cha
        print(f"Nguoi nop : {self.nguoi_nop}")
 
 
class PhieuChi(ChungTu):
    """Phiếu chi tiền mặt."""
 
    NGUONG_CAN_DUYET = 20_000_000
 
    def __init__(self, so, ngay, so_tien, nguoi_nhan, dien_giai=""):
        super().__init__(so, ngay, so_tien, dien_giai)
        self.nguoi_nhan = nguoi_nhan
 
    def mo_ta(self):
        return "PHIEU CHI"
 
    def can_giam_doc_duyet(self):
        """Chi trên ngưỡng phải có Giám đốc duyệt."""
        return self.so_tien >= self.NGUONG_CAN_DUYET
 
    def in_chung_tu(self):
        super().in_chung_tu()
        print(f"Nguoi nhan: {self.nguoi_nhan}")
        if self.can_giam_doc_duyet():
            print("LUU Y     : Can Giam doc phe duyet")
 
 
pt = PhieuThu("PT001", "08/08/2026", 5_000_000, "Nguyen Van A", "Thu tien hang")
pc = PhieuChi("PC001", "08/08/2026", 25_000_000, "Cong ty XYZ", "Thanh toan NCC")
 
pt.in_chung_tu()
print()
pc.in_chung_tu()

6.1. Thuật ngữ

Thuật ngữNghĩa
Base class / Parent class (lớp cha)ChungTu
Derived class / Child class (lớp con)PhieuThu, PhieuChi
Override (ghi đè)Class con định nghĩa lại method của cha
super()Gọi method của class cha

6.2. isinstance() và issubclass()

print(isinstance(pt, PhieuThu))       # True
print(isinstance(pt, ChungTu))        # True  — PhieuThu LÀ MỘT ChungTu
print(isinstance(pt, PhieuChi))       # False
print(issubclass(PhieuThu, ChungTu))  # True

7. Dunder methods — Phương thức đặc biệt

Các method có dạng __ten__ được Python tự gọi trong tình huống nhất định.

7.1. __str__ và __repr__

class SanPham:
    def __init__(self, ma, ten, gia):
        self.ma = ma
        self.ten = ten
        self.gia = gia
 
    def __str__(self):
        """Dành cho NGƯỜI ĐỌC — gọi bởi print() và str()."""
        return f"{self.ten} ({self.gia:,.0f} VND)"
 
    def __repr__(self):
        """Dành cho LẬP TRÌNH VIÊN — hiện trong REPL, debug, list."""
        return f"SanPham(ma={self.ma!r}, ten={self.ten!r}, gia={self.gia})"
 
 
sp = SanPham("VT001", "Ban lam viec", 2_500_000)
 
print(sp)                # Ban lam viec (2.500.000 VND)   <- __str__
print(str(sp))           # Ban lam viec (2.500.000 VND)
print(repr(sp))          # SanPham(ma='VT001', ten='Ban lam viec', gia=2500000)
print([sp])              # [SanPham(ma='VT001', ...)]     <- list dùng __repr__

Không có __str__/__repr__:

print(sp)     # <__main__.SanPham object at 0x000001A2B3C4D5E0>  — vô dụng

7.2. __eq__ — So sánh bằng

class SanPham:
    def __init__(self, ma, ten, gia):
        self.ma = ma
        self.ten = ten
        self.gia = gia
 
    def __eq__(self, khac):
        """Hai sản phẩm bằng nhau nếu cùng mã."""
        if not isinstance(khac, SanPham):
            return NotImplemented
        return self.ma == khac.ma
 
    def __hash__(self):
        """Cần thiết để dùng được trong set và làm khóa dict."""
        return hash(self.ma)
 
 
a = SanPham("VT001", "Ban", 2_500_000)
b = SanPham("VT001", "Ban lam viec", 2_600_000)
print(a == b)            # True — cùng mã
print({a, b})            # Chỉ 1 phần tử trong set

7.3. __len__, __lt__, __add__

class HoaDon:
    def __init__(self, so):
        self.so = so
        self.chi_tiet = []
 
    def them(self, ten, sl, gia):
        self.chi_tiet.append({"ten": ten, "sl": sl, "gia": gia})
        return self
 
    def tong_tien(self):
        return sum(m["sl"] * m["gia"] for m in self.chi_tiet)
 
    def __len__(self):
        """len(hoa_don) trả về số dòng chi tiết."""
        return len(self.chi_tiet)
 
    def __lt__(self, khac):
        """Cho phép so sánh < và dùng sorted()."""
        return self.tong_tien() < khac.tong_tien()
 
    def __str__(self):
        return f"HD {self.so}: {len(self)} dong, {self.tong_tien():,.0f} VND"
 
 
hd1 = HoaDon("HD001")
hd1.them("Ban", 3, 2_500_000).them("Ghe", 10, 850_000)
 
hd2 = HoaDon("HD002")
hd2.them("Tu", 2, 4_200_000)
 
print(len(hd1))                       # 2
print(hd1 > hd2)                      # True
for hd in sorted([hd1, hd2]):
    print(hd)

7.4. Bảng dunder methods thường dùng

MethodĐược gọi khi
__init__Tạo đối tượng
__str__print(x), str(x), f-string
__repr__repr(x), hiện trong list/REPL
__len__len(x)
__eq__x == y
__lt__, __gt__, __le__, __ge__<, >, <=, >=
__add__, __sub__, __mul__+, -, *
__contains__x in y
__getitem__x[i]
__enter__, __exit__with x:

PHẦN 2 — TUẦN 23

8. Polymorphism — Đa hình

Polymorphism nghĩa là các đối tượng khác nhau phản ứng khác nhau với cùng một lời gọi.

chung_tu = [
    PhieuThu("PT001", "08/08/2026", 5_000_000, "Nguyen Van A"),
    PhieuChi("PC001", "08/08/2026", 25_000_000, "Cong ty XYZ"),
    PhieuThu("PT002", "09/08/2026", 3_200_000, "Tran Thi B"),
]
 
for ct in chung_tu:
    print(ct.mo_ta())      # Mỗi loại tự trả về mô tả riêng
PHIEU THU
PHIEU CHI
PHIEU THU

Code gọi ct.mo_ta() không cần biết ct là loại chứng từ nào. Đây là sức mạnh chính của OOP: thêm loại chứng từ mới không phải sửa code cũ.

8.1. Duck Typing

Python không đòi hỏi kế thừa để có đa hình. Chỉ cần đối tượng có method đó là dùng được:

“If it walks like a duck and quacks like a duck, it’s a duck.”

class MayIn:
    def in_ra(self, noi_dung):
        print(f"[MAY IN] {noi_dung}")
 
class FileLog:
    def in_ra(self, noi_dung):
        with open("log.txt", "a", encoding="utf-8") as f:
            f.write(noi_dung + "\n")
 
class ManHinh:
    def in_ra(self, noi_dung):
        print(noi_dung)
 
 
def xuat_bao_cao(thiet_bi, du_lieu):
    """Xuất báo cáo ra bất kỳ thiết bị nào có method in_ra()."""
    for dong in du_lieu:
        thiet_bi.in_ra(dong)
 
 
du_lieu = ["BAO CAO CONG NO", "KH001: 5,000,000", "KH002: 12,000,000"]
 
xuat_bao_cao(ManHinh(), du_lieu)
xuat_bao_cao(MayIn(), du_lieu)

Ba class trên không hề kế thừa nhau, nhưng đều dùng được vì đều có in_ra().


9. Abstraction — Trừu tượng hóa

from abc import ABC, abstractmethod
 
 
class PhuongThucThanhToan(ABC):
    """Class trừu tượng — không tạo đối tượng trực tiếp được."""
 
    @abstractmethod
    def thanh_toan(self, so_tien):
        """Mọi class con BẮT BUỘC phải cài đặt method này."""
        ...
 
    @abstractmethod
    def phi_giao_dich(self, so_tien):
        ...
 
    def tong_chi_phi(self, so_tien):
        """Method thường — class con dùng chung."""
        return so_tien + self.phi_giao_dich(so_tien)
 
 
class TienMat(PhuongThucThanhToan):
    def thanh_toan(self, so_tien):
        return f"Thanh toan {so_tien:,.0f} VND bang tien mat"
 
    def phi_giao_dich(self, so_tien):
        return 0
 
 
class ChuyenKhoan(PhuongThucThanhToan):
    PHI_CO_DINH = 11_000
 
    def thanh_toan(self, so_tien):
        return f"Chuyen khoan {so_tien:,.0f} VND"
 
    def phi_giao_dich(self, so_tien):
        return self.PHI_CO_DINH
 
 
class TheTinDung(PhuongThucThanhToan):
    TY_LE_PHI = 0.02
 
    def thanh_toan(self, so_tien):
        return f"Quet the {so_tien:,.0f} VND"
 
    def phi_giao_dich(self, so_tien):
        return so_tien * self.TY_LE_PHI
 
 
# PhuongThucThanhToan()    # ❌ TypeError: Can't instantiate abstract class
 
so_tien = 10_000_000
for pt in [TienMat(), ChuyenKhoan(), TheTinDung()]:
    print(f"{pt.thanh_toan(so_tien)}")
    print(f"  Phi: {pt.phi_giao_dich(so_tien):,.0f} | "
          f"Tong: {pt.tong_chi_phi(so_tien):,.0f}")

ABC = Abstract Base Class. Nó ép buộc class con phải cài đặt các method bắt buộc — nếu quên sẽ báo lỗi ngay khi tạo đối tượng, thay vì lỗi âm thầm lúc chạy.


10. @dataclass — Cách viết class gọn (Python 3.7+)

10.1. Vấn đề

class SanPham:
    def __init__(self, ma, ten, gia, so_luong=0):
        self.ma = ma
        self.ten = ten
        self.gia = gia
        self.so_luong = so_luong
 
    def __repr__(self):
        return (f"SanPham(ma={self.ma!r}, ten={self.ten!r}, "
                f"gia={self.gia}, so_luong={self.so_luong})")
 
    def __eq__(self, khac):
        if not isinstance(khac, SanPham):
            return NotImplemented
        return (self.ma, self.ten, self.gia, self.so_luong) == \
               (khac.ma, khac.ten, khac.gia, khac.so_luong)

Rất nhiều code lặp lại chỉ để lưu 4 giá trị.

10.2. Giải pháp

from dataclasses import dataclass, field
 
 
@dataclass
class SanPham:
    """Sản phẩm trong danh mục hàng hóa."""
    ma: str
    ten: str
    gia: float
    so_luong: int = 0
    ghi_chu: str = ""
 
    def thanh_tien(self):
        """Tính giá trị tồn kho của sản phẩm."""
        return self.gia * self.so_luong
 
 
sp1 = SanPham("VT001", "Ban lam viec", 2_500_000, 3)
sp2 = SanPham("VT001", "Ban lam viec", 2_500_000, 3)
 
print(sp1)              # SanPham(ma='VT001', ten='Ban lam viec', gia=2500000, so_luong=3, ghi_chu='')
print(sp1 == sp2)       # True  — __eq__ tự sinh
print(sp1.thanh_tien()) # 7500000

@dataclass tự sinh __init__, __repr__, __eq__ từ danh sách thuộc tính.

10.3. Các tùy chọn hữu ích

from dataclasses import dataclass, field
from datetime import date
 
 
@dataclass(frozen=True)              # frozen = bất biến (immutable)
class DonViTinh:
    ma: str
    ten: str
 
dvt = DonViTinh("CAI", "Cái")
# dvt.ma = "HOP"      # ❌ FrozenInstanceError
 
 
@dataclass(order=True)               # order = tự sinh <, >, <=, >=
class MucDo:
    cap: int
    ten: str
 
print(MucDo(1, "Thap") < MucDo(3, "Cao"))    # True
 
 
@dataclass
class HoaDon:
    so: str
    ngay: date
    chi_tiet: list = field(default_factory=list)    # ✅ Cách đúng cho list mặc định
    # chi_tiet: list = []                           # ❌ ValueError khi định nghĩa class

field(default_factory=list)

@dataclass chủ động chặn bẫy mutable default đã học ở 10 - Hàm (Function). Nếu bạn viết chi_tiet: list = [], Python báo lỗi ngay khi định nghĩa class, buộc bạn dùng default_factory.


11. Khi nào dùng OOP, khi nào không?

Dùng classKhông cần class
Dữ liệu và hành vi gắn liềnChỉ chứa dữ liệu → dùng dict hoặc dataclass
Cần kiểm soát khi gán giá trịScript nhỏ, chạy một lần
Nhiều biến thể chung một khuôn (đa hình)Hàm thuần túy (input → output)
Đối tượng có trạng thái thay đổi theo thời gianXử lý dữ liệu một chiều

Đừng lạm dụng OOP

Không phải mọi thứ đều cần class. Một script 50 dòng đọc CSV và in báo cáo thì hàm là đủ. Java/C# buộc mọi thứ vào class, Python thì không — và đó là điểm mạnh.

Nguyên tắc: bắt đầu bằng hàm, chuyển sang class khi thấy nhiều hàm cùng thao tác trên một cụm dữ liệu.


12. Chương trình mẫu tổng hợp

"""
Hệ thống quản lý chứng từ kế toán — minh họa OOP đầy đủ.
"""
 
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import date
from decimal import Decimal
 
 
# ==================== DATACLASS: dữ liệu thuần ====================
 
@dataclass(frozen=True)
class TaiKhoan:
    """Tài khoản kế toán theo hệ thống tài khoản."""
    so: str
    ten: str
 
    def __str__(self):
        return f"{self.so} - {self.ten}"
 
 
@dataclass
class DinhKhoan:
    """Một bút toán định khoản Nợ/Có."""
    tk_no: TaiKhoan
    tk_co: TaiKhoan
    so_tien: Decimal
    dien_giai: str = ""
 
 
# ==================== ABSTRACT BASE CLASS ====================
 
class ChungTu(ABC):
    """Class trừu tượng cho mọi loại chứng từ kế toán."""
 
    dem_chung_tu = 0        # Class attribute — đếm tổng số chứng từ
 
    def __init__(self, so, ngay, dien_giai=""):
        self.so = so
        self.ngay = ngay
        self.dien_giai = dien_giai
        self._dinh_khoan = []
        self._da_duyet = False
        ChungTu.dem_chung_tu += 1
 
    # ----- Property -----
    @property
    def tong_no(self):
        """Tổng phát sinh Nợ."""
        return sum(dk.so_tien for dk in self._dinh_khoan)
 
    @property
    def tong_co(self):
        """Tổng phát sinh Có."""
        return sum(dk.so_tien for dk in self._dinh_khoan)
 
    @property
    def da_duyet(self):
        return self._da_duyet
 
    @property
    def can_doi(self):
        """Chứng từ có cân đối Nợ = Có không."""
        return self.tong_no == self.tong_co
 
    # ----- Abstract methods -----
    @abstractmethod
    def loai(self):
        """Tên loại chứng từ."""
        ...
 
    @abstractmethod
    def tien_to_ma(self):
        """Tiền tố mã chứng từ."""
        ...
 
    # ----- Methods thường -----
    def them_dinh_khoan(self, tk_no, tk_co, so_tien, dien_giai=""):
        """Thêm một bút toán định khoản."""
        if self._da_duyet:
            raise ValueError("Khong the sua chung tu da duyet")
        if so_tien <= 0:
            raise ValueError(f"So tien phai lon hon 0, nhan duoc {so_tien}")
        self._dinh_khoan.append(DinhKhoan(tk_no, tk_co, so_tien, dien_giai))
        return self
 
    def duyet(self):
        """Duyệt chứng từ sau khi kiểm tra hợp lệ."""
        if not self._dinh_khoan:
            raise ValueError("Chung tu chua co dinh khoan nao")
        if not self.can_doi:
            raise ValueError(
                f"Chung tu khong can doi: No={self.tong_no:,}, Co={self.tong_co:,}"
            )
        self._da_duyet = True
        return self
 
    def in_chung_tu(self, do_rong=70):
        """In chứng từ ra màn hình."""
        print("=" * do_rong)
        print(self.loai().upper().center(do_rong))
        print(f"So: {self.so}   Ngay: {self.ngay:%d/%m/%Y}".center(do_rong))
        print("=" * do_rong)
        if self.dien_giai:
            print(f"Dien giai: {self.dien_giai}")
            print("-" * do_rong)
 
        print(f"{'TK NO':<12}{'TK CO':<12}{'DIEN GIAI':<28}{'SO TIEN':>18}")
        print("-" * do_rong)
        for dk in self._dinh_khoan:
            print(f"{dk.tk_no.so:<12}{dk.tk_co.so:<12}"
                  f"{dk.dien_giai[:27]:<28}{dk.so_tien:>18,.0f}")
        print("-" * do_rong)
        print(f"{'TONG CONG':<52}{self.tong_no:>18,.0f}")
        print("=" * do_rong)
        print(f"Trang thai: {'DA DUYET' if self._da_duyet else 'CHUA DUYET'}")
        print()
 
    def __len__(self):
        return len(self._dinh_khoan)
 
    def __str__(self):
        return f"{self.loai()} {self.so} - {self.tong_no:,.0f} VND"
 
    def __repr__(self):
        return f"{type(self).__name__}(so={self.so!r}, ngay={self.ngay!r})"
 
 
# ==================== CÁC CLASS CON ====================
 
class PhieuThu(ChungTu):
    """Phiếu thu tiền mặt."""
 
    def __init__(self, so, ngay, nguoi_nop, dien_giai=""):
        super().__init__(so, ngay, dien_giai)
        self.nguoi_nop = nguoi_nop
 
    def loai(self):
        return "Phieu thu"
 
    def tien_to_ma(self):
        return "PT"
 
    def in_chung_tu(self, do_rong=70):
        super().in_chung_tu(do_rong)
        print(f"Nguoi nop tien: {self.nguoi_nop}")
        print()
 
 
class PhieuChi(ChungTu):
    """Phiếu chi tiền mặt."""
 
    NGUONG_GIAM_DOC = Decimal("20000000")
 
    def __init__(self, so, ngay, nguoi_nhan, dien_giai=""):
        super().__init__(so, ngay, dien_giai)
        self.nguoi_nhan = nguoi_nhan
 
    def loai(self):
        return "Phieu chi"
 
    def tien_to_ma(self):
        return "PC"
 
    @property
    def can_giam_doc_duyet(self):
        return self.tong_no >= self.NGUONG_GIAM_DOC
 
    def in_chung_tu(self, do_rong=70):
        super().in_chung_tu(do_rong)
        print(f"Nguoi nhan tien: {self.nguoi_nhan}")
        if self.can_giam_doc_duyet:
            print(">> CAN GIAM DOC PHE DUYET (tren 20 trieu)")
        print()
 
 
# ==================== SỬ DỤNG ====================
 
def main():
    # Danh mục tài khoản
    tk_111 = TaiKhoan("111", "Tien mat")
    tk_112 = TaiKhoan("112", "Tien gui ngan hang")
    tk_131 = TaiKhoan("131", "Phai thu khach hang")
    tk_331 = TaiKhoan("331", "Phai tra nguoi ban")
 
    # Phiếu thu
    pt = PhieuThu("PT000001", date(2026, 8, 8), "Cong ty TNHH An Phat",
                  "Thu tien hang thang 07/2026")
    pt.them_dinh_khoan(tk_111, tk_131, Decimal("15000000"), "Thu tien mat")
    pt.duyet()
    pt.in_chung_tu()
 
    # Phiếu chi
    pc = PhieuChi("PC000001", date(2026, 8, 8), "Cong ty CP Vat tu XYZ",
                  "Thanh toan tien hang")
    pc.them_dinh_khoan(tk_331, tk_112, Decimal("25000000"), "Chuyen khoan tra NCC")
    pc.duyet()
    pc.in_chung_tu()
 
    # Đa hình — xử lý chung
    print("=" * 70)
    print("DANH SACH CHUNG TU".center(70))
    print("=" * 70)
    for ct in [pt, pc]:
        print(f"  {ct}  ({len(ct)} dong dinh khoan)")
    print(f"\nTong so chung tu da tao: {ChungTu.dem_chung_tu}")
 
    # Thử tạo chứng từ không cân đối
    print()
    pc2 = PhieuChi("PC000002", date(2026, 8, 9), "Nha cung cap ABC")
    try:
        pc2.duyet()
    except ValueError as e:
        print(f"Khong the duyet: {e}")
 
 
if __name__ == "__main__":
    main()

13. Thực hành bắt buộc

Bài 1 — Class cơ bản

Viết class HinhChuNhat:

  • Thuộc tính: chieu_dai, chieu_rong
  • Method: dien_tich(), chu_vi(), la_hinh_vuong()
  • __str__ in ra "Hình chữ nhật 5.0 x 3.0"

Bài 2 — @property

Viết class NhietDo:

  • Lưu nội bộ theo độ C (_celsius)
  • Property celsius (đọc/ghi, không cho dưới -273.15)
  • Property fahrenheit (đọc/ghi, tự quy đổi)
  • Property kelvin (chỉ đọc)

Bài 3 — Class SanPham

Viết class SanPham với:

  • Thuộc tính: mã, tên, đơn giá, số lượng tồn
  • nhap_kho(sl), xuat_kho(sl) — kiểm tra đủ tồn, ném lỗi nếu không
  • Property gia_tri_ton (chỉ đọc)
  • __str__, __repr__, __eq__ (bằng nhau khi cùng mã)

Bài 4 — Kế thừa

Xây cây class nhân viên:

NhanVien (ho_ten, ma_nv, luong_co_ban)
├── NhanVienVanPhong (+ phu_cap)
├── NhanVienKinhDoanh (+ doanh_so, ty_le_hoa_hong)
└── QuanLy (+ so_nhan_vien_quan_ly, phu_cap_quan_ly)

Mỗi class có method tinh_luong() riêng. Tạo danh sách trộn lẫn các loại, dùng đa hình để in bảng lương.

Bài 5 — @dataclass

Viết lại class SanPham ở bài 3 bằng @dataclass. So sánh số dòng code.

Thêm một @dataclass(frozen=True) cho DonViTinh.

Bài 6 — Abstract Base Class

Viết ABC tên BaoCao với abstract method tao_noi_dung() và dinh_dang(). Cài đặt 3 class con: BaoCaoText, BaoCaoCSV, BaoCaoJSON.

Bài 7 — Refactor toàn diện

Lấy chương trình so_thu_chi.py bạn viết ở 13 - Làm việc với File bài 8, viết lại theo OOP:

  • Class GiaoDich (dùng @dataclass)
  • Class SoThuChi — quản lý danh sách giao dịch, có method thêm/sửa/xóa/lọc/thống kê
  • Class KhoLuuTru — lo việc đọc/ghi JSON
  • So sánh với bản cũ: bản nào dễ mở rộng hơn?

Bài 8 — Dunder methods

Viết class Tien biểu diễn số tiền có đơn vị:

  • __add__, __sub__ — cộng trừ (báo lỗi nếu khác đơn vị)
  • __mul__ — nhân với số
  • __lt__, __eq__ — so sánh
  • __str__ — "1.234.567 VND"

14. Tự kiểm tra

  • Tôi giải thích được class và object khác nhau thế nào
  • Tôi hiểu vai trò của self
  • Tôi phân biệt class attribute và instance attribute
  • Tôi biết bẫy mutable class attribute
  • Tôi dùng được @property để kiểm soát việc gán giá trị
  • Tôi viết được class kế thừa và dùng super()
  • Tôi biết __str__ khác __repr__
  • Tôi hiểu đa hình và duck typing
  • Tôi biết dùng @dataclass
  • Tôi biết khi nào không nên dùng class

15. Tài liệu chính thức

NguồnNội dungLink
Tutorial — ClassesNhập môn OOPhttps://docs.python.org/3/tutorial/classes.html
Data model — Special method namesToàn bộ dunder methodshttps://docs.python.org/3/reference/datamodel.html#special-method-names
dataclassesTài liệu dataclasshttps://docs.python.org/3/library/dataclasses.html
abc moduleAbstract Base Classhttps://docs.python.org/3/library/abc.html
property()Tài liệu propertyhttps://docs.python.org/3/library/functions.html#property
PEP 557 — Data ClassesĐề xuất gốc của dataclasshttps://peps.python.org/pep-0557/

Tiếp theo → 15 - Môi trường ảo và pip