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ệu
kh["cong_no"] = -999999 — không ai ngăn được
Dễ gõ sai khóa
kh["cog_no"] → KeyError chỉ khi chạy tới
Không biết có khóa nào
Phải nhớ hoặc print ra xem
Dữ liệu và hành vi tách rời
tinh_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 OOP
Tươ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 rowkh2 = 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) # KH001print(kh1.cong_no) # 5000000print(kh2.cong_no) # 0 — dùng giá trị mặc định
2.1. Mổ xẻ
Thành phần
Vai 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
self
Tham chiếu tới chính đối tượng đang được thao tác
self.ma = ma
Gá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 kh1kh2.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 = maclass B: def __init__(self, ma): ma = ma # ❌ Gán biến cục bộ, không phải thuộc tínhclass 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 dateclass 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 class
Trong class
Tham số đầu
Bất kỳ
self
Cách gọi
ham(doi_tuong)
doi_tuong.method()
# Functiondef thanh_toan(kh_dict, so_tien): kh_dict["cong_no"] -= so_tienthanh_toan(kh, 1000)# Methodkh.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 += 1kh1 = KhachHang("KH001", "A")kh2 = KhachHang("KH002", "B")kh3 = KhachHang("KH003", "C")print(KhachHang.so_luong_khach) # 3print(kh1.so_luong_khach) # 3 — mọi đối tượng đều thấyprint(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
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ĩa
Python có chặn không?
self.ten
Public — 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_mat
Name 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_dutk = 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 VNDtk.nap(500_000)print(tk.so_du_dinh_dang) # 1.500.000 VNDtk.so_du = -100 # ❌ ValueError: So du khong the amtk.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)) # Trueprint(isinstance(pt, ChungTu)) # True — PhieuThu LÀ MỘT ChungTuprint(isinstance(pt, PhieuChi)) # Falseprint(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)) # 2print(hd1 > hd2) # Truefor 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ếtct 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, abstractmethodclass 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 0class 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_DINHclass 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 classso_tien = 10_000_000for 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@dataclassclass 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_luongsp1 = 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ự sinhprint(sp1.thanh_tien()) # 7500000
@dataclasstự 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, fieldfrom datetime import date@dataclass(frozen=True) # frozen = bất biến (immutable)class DonViTinh: ma: str ten: strdvt = DonViTinh("CAI", "Cái")# dvt.ma = "HOP" # ❌ FrozenInstanceError@dataclass(order=True) # order = tự sinh <, >, <=, >=class MucDo: cap: int ten: strprint(MucDo(1, "Thap") < MucDo(3, "Cao")) # True@dataclassclass 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)
@dataclasschủ độ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 class
Không cần class
Dữ liệu và hành vi gắn liền
Chỉ 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 gian
Xử 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, abstractmethodfrom dataclasses import dataclass, fieldfrom datetime import datefrom 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}"@dataclassclass 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