Bài học: Lập Trình Hướng Đối Tượng (OOP) trong Python
Bài học dành cho người mới bắt đầu — lý thuyết dễ hiểu, gắn với ví dụ thực tế, kèm bài tập thực hành ở cuối mỗi phần.
Mục lục
- OOP là gì? Tại sao cần học?
- Class và Object
- Hàm khởi tạo
__init__ - Thuộc tính và Phương thức
- Tính kế thừa (Inheritance)
- Tính đa hình (Polymorphism)
- Tính đóng gói (Encapsulation)
- Trừu tượng hóa (Abstraction)
- Magic Methods
- Bài tập tổng hợp cuối bài
1. OOP là gì? Tại sao cần học?
1.1 Vấn đề khi không dùng OOP
Giả sử bạn cần quản lý thông tin sinh viên: tên, tuổi, điểm. Nếu không dùng OOP, bạn có thể viết:
ten1 = "An"
tuoi1 = 20
diem1 = 8.5
ten2 = "Binh"
tuoi2 = 21
diem2 = 7.0
Với 100 sinh viên, bạn sẽ có... 300 biến rời rạc, rất khó quản lý và dễ nhầm lẫn.
1.2 OOP giải quyết vấn đề này như thế nào?
OOP (Object-Oriented Programming) cho phép bạn gom nhóm dữ liệu và hành vi liên quan vào một "đối tượng" — giống như ngoài đời thực, mỗi sinh viên là một thực thể có tên, tuổi, điểm và có thể "làm bài", "xem điểm"...
Liên hệ thực tế: Hãy nghĩ về một chiếc điện thoại. Nó có thuộc tính (màu sắc, dung lượng pin, thương hiệu) và hành vi (gọi điện, chụp ảnh, sạc pin). OOP mô phỏng đúng cách chúng ta nhìn nhận sự vật trong đời thực.
1.3 Bốn trụ cột của OOP
| Trụ cột | Ý nghĩa đơn giản |
|---|---|
| Inheritance (Kế thừa) | Một class có thể "thừa hưởng" đặc điểm từ class khác |
| Polymorphism (Đa hình) | Nhiều đối tượng khác nhau có thể phản ứng khác nhau với cùng một hành động |
| Encapsulation (Đóng gói) | Giấu chi tiết bên trong, chỉ lộ ra những gì cần thiết |
| Abstraction (Trừu tượng) | Chỉ quan tâm đến "làm gì", không cần biết "làm như thế nào" |
Các phần tiếp theo của bài học sẽ đi sâu vào từng khái niệm này.
2. Class và Object
2.1 Khái niệm
- Class (Lớp): là bản thiết kế/khuôn mẫu. Ví dụ: bản vẽ thiết kế "xe hơi".
- Object (Đối tượng): là sản phẩm cụ thể được tạo ra từ bản thiết kế đó. Ví dụ: chiếc xe Toyota màu đỏ của bạn.
Liên hệ thực tế: Class giống như khuôn bánh, Object giống như từng cái bánh được làm ra từ khuôn đó. Một khuôn có thể tạo ra nhiều bánh khác nhau về nhân, màu sắc nhưng cùng hình dạng.
2.2 Cú pháp tạo Class trong Python
class Xe:
pass # class rỗng, chưa có gì bên trong
# Tạo object (thực thể) từ class Xe
xe1 = Xe()
xe2 = Xe()
print(type(xe1)) # <class '__main__.Xe'>
print(xe1 is xe2) # False — hai object khác nhau, dù cùng class
2.3 Quy ước đặt tên
- Tên Class: viết hoa chữ cái đầu mỗi từ —
PascalCase. Ví dụ:HocSinh,TaiKhoanNganHang. - Tên biến, hàm: viết thường, cách nhau bằng dấu
_—snake_case. Ví dụ:hoc_sinh_1,tinh_diem().
3. Hàm khởi tạo __init__
3.1 __init__ là gì?
__init__ là phương thức đặc biệt được Python tự động gọi ngay khi object được tạo ra. Nó dùng để khởi tạo giá trị ban đầu cho các thuộc tính.
Liên hệ thực tế: Giống như khi bạn đăng ký tài khoản ngân hàng, nhân viên sẽ hỏi bạn tên, số CMND, số tiền gửi ban đầu — đó chính là quá trình "khởi tạo" một tài khoản mới.
class TaiKhoanNganHang:
def __init__(self, chu_tai_khoan, so_du_ban_dau=0):
self.chu_tai_khoan = chu_tai_khoan
self.so_du = so_du_ban_dau
print(f"Đã tạo tài khoản cho {chu_tai_khoan} với số dư {so_du_ban_dau:,}đ")
tk1 = TaiKhoanNganHang("Nguyễn Văn A", 1000000)
# Đã tạo tài khoản cho Nguyễn Văn A với số dư 1,000,000đ
3.2 Tham số mặc định
Bạn có thể đặt giá trị mặc định cho tham số, giống ví dụ trên (so_du_ban_dau=0). Nếu không truyền, Python sẽ tự lấy giá trị mặc định:
tk2 = TaiKhoanNganHang("Trần Thị B")
# Đã tạo tài khoản cho Trần Thị B với số dư 0đ
4. Thuộc tính và Phương thức
4.1 Thuộc tính — Dữ liệu của đối tượng
class Xe:
def __init__(self, hang, mau):
self.hang = hang # thuộc tính (attribute)
self.mau = mau
xe1 = Xe("Toyota", "Đỏ")
print(xe1.hang) # Toyota
print(xe1.mau) # Đỏ
4.2 Phương thức — Hành vi của đối tượng
Phương thức (method) là hàm được định nghĩa bên trong class, mô tả những gì đối tượng "có thể làm".
class Xe:
def __init__(self, hang, mau, xang):
self.hang = hang
self.mau = mau
self.xang = xang # % xăng còn lại
def chay(self):
if self.xang > 0:
self.xang -= 10
print(f"Xe {self.hang} đang chạy. Xăng còn {self.xang}%")
else:
print("Hết xăng rồi, cần đổ xăng!")
xe1 = Xe("Honda", "Đen", 50)
xe1.chay() # Xe Honda đang chạy. Xăng còn 40%
xe1.chay() # Xe Honda đang chạy. Xăng còn 30%
Lưu ý quan trọng về
self:selfđại diện cho chính đối tượng đang gọi phương thức đó. Khi bạn gọixe1.chay(), Python tự động truyềnxe1vào làmself. Nhờ vậy phương thức biết cần thao tác trên dữ liệu củaxe1chứ không phảixe2.
4.3 Phân biệt thuộc tính instance và thuộc tính class
class HocSinh:
truong = "THPT Nguyễn Trãi" # thuộc tính class — DÙNG CHUNG cho mọi object
def __init__(self, ten):
self.ten = ten # thuộc tính instance — RIÊNG cho từng object
hs1 = HocSinh("An")
hs2 = HocSinh("Bình")
print(hs1.truong) # THPT Nguyễn Trãi
print(hs2.truong) # THPT Nguyễn Trãi
print(hs1.ten) # An
print(hs2.ten) # Bình
5. Tính kế thừa (Inheritance)
5.1 Ý tưởng
Liên hệ thực tế: "Chó" và "Mèo" đều là "Động vật" — chúng đều biết ăn, ngủ, di chuyển. Nhưng "Chó" thì biết sủa, "Mèo" thì biết kêu meo. Thay vì viết lại toàn bộ đặc điểm chung cho từng loài, ta định nghĩa class
DongVatchứa đặc điểm chung, rồi choChovàMeokế thừa từ đó.
5.2 Cú pháp cơ bản
class DongVat:
def __init__(self, ten):
self.ten = ten
def an(self):
print(f"{self.ten} đang ăn")
def ngu(self):
print(f"{self.ten} đang ngủ")
class Cho(DongVat): # Cho kế thừa từ DongVat
def sua(self):
print(f"{self.ten} sủa: Gâu gâu!")
class Meo(DongVat): # Meo kế thừa từ DongVat
def keu(self):
print(f"{self.ten} kêu: Meo meo!")
cho1 = Cho("Lu")
cho1.an() # Lu đang ăn (kế thừa từ DongVat)
cho1.sua() # Lu sủa: Gâu gâu! (riêng của Cho)
meo1 = Meo("Mun")
meo1.ngu() # Mun đang ngủ (kế thừa từ DongVat)
meo1.keu() # Mun kêu: Meo meo! (riêng của Meo)
5.3 Ghi đè phương thức (Method Overriding) và super()
Class con có thể định nghĩa lại một phương thức của class cha để thay đổi hành vi cho phù hợp:
class NhanVien:
def __init__(self, ten, luong_co_ban):
self.ten = ten
self.luong_co_ban = luong_co_ban
def tinh_luong(self):
return self.luong_co_ban
class QuanLy(NhanVien):
def __init__(self, ten, luong_co_ban, phu_cap):
super().__init__(ten, luong_co_ban) # gọi __init__ của class cha
self.phu_cap = phu_cap
def tinh_luong(self): # ghi đè phương thức của class cha
return self.luong_co_ban + self.phu_cap
nv = NhanVien("An", 10000000)
ql = QuanLy("Bình", 15000000, 5000000)
print(nv.tinh_luong()) # 10000000
print(ql.tinh_luong()) # 20000000
super() cho phép class con gọi lại phương thức của class cha thay vì viết trùng lặp code.
6. Tính đa hình (Polymorphism)
6.1 Ý tưởng
Đa hình nghĩa là "nhiều hình dạng" — cùng một hành động (tên phương thức giống nhau) nhưng mỗi đối tượng lại thực hiện theo cách riêng của mình.
Liên hệ thực tế: Bạn bấm nút "Play" (▶️) — trên loa nó phát nhạc, trên TV nó phát video, trên máy chiếu nó chiếu phim. Cùng một hành động "Play" nhưng mỗi thiết bị phản ứng khác nhau.
6.2 Ví dụ minh họa
class HinhVuong:
def __init__(self, canh):
self.canh = canh
def dien_tich(self):
return self.canh ** 2
class HinhTron:
def __init__(self, ban_kinh):
self.ban_kinh = ban_kinh
def dien_tich(self):
return 3.14 * self.ban_kinh ** 2
class HinhChuNhat:
def __init__(self, dai, rong):
self.dai = dai
self.rong = rong
def dien_tich(self):
return self.dai * self.rong
danh_sach_hinh = [HinhVuong(4), HinhTron(3), HinhChuNhat(5, 2)]
for hinh in danh_sach_hinh:
# Không cần biết đó là hình gì, chỉ cần gọi dien_tich()
print(f"{type(hinh).__name__}: {hinh.dien_tich():.2f}")
# HinhVuong: 16.00
# HinhTron: 28.26
# HinhChuNhat: 10.00
Đây chính là sức mạnh của đa hình: bạn viết một vòng lặp for duy nhất, xử lý được nhiều loại đối tượng khác nhau, miễn là chúng đều có phương thức dien_tich().
7. Tính đóng gói (Encapsulation)
7.1 Tại sao cần đóng gói?
Liên hệ thực tế: Bạn không thể tự ý mở nắp capo và chỉnh sửa động cơ xe hơi ngay khi đang chạy trên đường — điều đó nguy hiểm. Nhà sản xuất giấu đi các chi tiết kỹ thuật phức tạp, chỉ cho bạn thao tác qua vô-lăng, chân ga, chân phanh. Đó chính là ý tưởng của Encapsulation: giấu dữ liệu nhạy cảm, chỉ cho phép truy cập qua các "cổng" được kiểm soát.
7.2 Quy ước đặt tên trong Python
| Tiền tố | Mức độ | Ý nghĩa | Ví dụ |
|---|---|---|---|
| Không tiền tố | Public | Truy cập tự do từ bất kỳ đâu | self.ten |
_ (1 gạch dưới) | Protected | Quy ước "chỉ nên dùng trong nội bộ class và class con" | self._so_du |
__ (2 gạch dưới) | Private | Python "làm rối tên" (name mangling) để hạn chế truy cập trực tiếp | self.__mat_khau |
7.2.1 Public — Truy cập tự do
Public là mức mặc định trong Python — thuộc tính/phương thức không có tiền tố gạch dưới. Bất kỳ đoạn code nào cũng có thể đọc và ghi giá trị này.
Khi nào dùng? Khi thông tin đó không nhạy cảm và không cần kiểm soát — ví dụ: tên sản phẩm, tên người dùng, tiêu đề bài viết.
class SinhVien:
def __init__(self, ten, ma_sv):
self.ten = ten # public — ai cũng đọc/ghi được
self.ma_sv = ma_sv # public
sv = SinhVien("An", "SV001")
print(sv.ten) # An — đọc bình thường
sv.ten = "Bình" # ghi bình thường
print(sv.ten) # Bình
7.2.2 Protected — Quy ước "nội bộ"
Protected dùng 1 dấu gạch dưới phía trước (_ten_thuoc_tinh). Đây chỉ là quy ước — Python không ngăn bạn truy cập từ bên ngoài, nhưng nó là tín hiệu cho lập trình viên khác biết: "Đừng đụng vào cái này từ bên ngoài class, trừ khi bạn là class con."
Liên hệ thực tế: Giống như biển "Chỉ dành cho nhân viên" — không có ổ khóa, nhưng bạn hiểu mình không nên vào.
class TaiKhoan:
def __init__(self, ten, so_du):
self.ten = ten
self._so_du = so_du # protected — "nội bộ, không nên đụng từ ngoài"
def xem_so_du(self):
return self._so_du # class tự dùng thuộc tính của mình
class TaiKhoanVip(TaiKhoan):
def uu_dai(self):
# Class con có thể truy cập _so_du — đây là mục đích của protected
if self._so_du >= 10_000_000:
return "Bạn được hưởng ưu đãi VIP!"
return "Chưa đủ điều kiện VIP."
tk = TaiKhoan("An", 5_000_000)
print(tk._so_du) # 5000000 — Python cho phép, nhưng không nên làm vậy!
vip = TaiKhoanVip("Bình", 15_000_000)
print(vip.uu_dai()) # Bạn được hưởng ưu đãi VIP!
Lưu ý:
_so_duvẫn truy cập được từ bên ngoài (tk._so_du). Protected chỉ là quy ước đạo đức lập trình, không phải rào cản kỹ thuật.
7.2.3 Private — Giới hạn thực sự bằng Name Mangling
Private dùng 2 dấu gạch dưới phía trước (__ten_thuoc_tinh). Khi gặp tiền tố này, Python tự động đổi tên thuộc tính ở cấp độ interpreter thành _TenClass__ten_thuoc_tinh — kỹ thuật này gọi là Name Mangling (làm rối tên). Nhờ vậy, truy cập trực tiếp từ bên ngoài sẽ thất bại với AttributeError.
Liên hệ thực tế: Giống như két sắt có mật mã — không chỉ là biển cấm, mà thực sự bị khóa lại. Muốn lấy tiền phải đi qua đúng quy trình.
class NguoiDung:
def __init__(self, ten, mat_khau):
self.ten = ten
self.__mat_khau = mat_khau # private
def kiem_tra_mat_khau(self, nhap_vao):
return nhap_vao == self.__mat_khau # class tự truy cập được
def doi_mat_khau(self, cu, moi):
if self.kiem_tra_mat_khau(cu):
self.__mat_khau = moi
print("Đổi mật khẩu thành công!")
else:
print("Mật khẩu cũ không đúng!")
nd = NguoiDung("An", "abc123")
# Truy cập đúng cách — qua phương thức
print(nd.kiem_tra_mat_khau("abc123")) # True
nd.doi_mat_khau("abc123", "xyz789") # Đổi mật khẩu thành công!
# Cố truy cập trực tiếp sẽ báo lỗi
# print(nd.__mat_khau) # AttributeError: 'NguoiDung' has no attribute '__mat_khau'
Cơ chế Name Mangling hoạt động thế nào?
class HopThu:
def __init__(self):
self.__bi_mat = "thông tin tối mật"
ht = HopThu()
# Truy cập thẳng — lỗi
# print(ht.__bi_mat) # AttributeError
# Python thực ra lưu với tên đã bị "làm rối":
print(ht._HopThu__bi_mat) # thông tin tối mật — vẫn truy cập được nếu biết tên thật!
print(dir(ht)) # thấy '_HopThu__bi_mat' trong danh sách thuộc tính
Kết luận: Private không phải là "bức tường thép" tuyệt đối — nó ngăn truy cập vô tình và nhầm lẫn, nhưng người có chủ đích vẫn có thể dùng tên đã bị mangling. Trong Python, triết lý là: "chúng ta đều là người lớn có trách nhiệm" — cơ chế bảo vệ là để tránh tai nạn, không phải để chống hack tuyệt đối.
7.2.4 So sánh tổng kết ba mức độ
| Mức độ | Cú pháp | Truy cập từ ngoài class | Truy cập từ class con | Cơ chế |
|---|---|---|---|---|
| Public | self.ten | Bình thường | Bình thường | Không giới hạn |
| Protected | self._ten | Được nhưng không nên | Khuyến khích | Chỉ là quy ước |
| Private | self.__ten | AttributeError | Không trực tiếp | Name Mangling |
class ViDu:
def __init__(self):
self.public = "ai cũng xem được"
self._protected = "chỉ nội bộ"
self.__private = "bị làm rối tên"
obj = ViDu()
print(obj.public) # ai cũng xem được
print(obj._protected) # chỉ nội bộ (được nhưng không nên)
# print(obj.__private) # AttributeError
print(obj._ViDu__private) # bị làm rối tên (name mangling)
7.3 Ví dụ thực tế: Tài khoản ngân hàng an toàn
class TaiKhoanNganHang:
def __init__(self, chu_tai_khoan, so_du_ban_dau=0):
self.chu_tai_khoan = chu_tai_khoan
self.__so_du = so_du_ban_dau # thuộc tính private
def xem_so_du(self):
return f"Số dư hiện tại: {self.__so_du:,}đ"
def nap_tien(self, so_tien):
if so_tien <= 0:
print("Số tiền nạp phải lớn hơn 0!")
return
self.__so_du += so_tien
print(f"Nạp thành công {so_tien:,}đ")
def rut_tien(self, so_tien):
if so_tien > self.__so_du:
print("Số dư không đủ!")
return
self.__so_du -= so_tien
print(f"Rút thành công {so_tien:,}đ")
tk = TaiKhoanNganHang("Nguyễn Văn A", 500000)
tk.nap_tien(200000)
tk.rut_tien(1000000) # Số dư không đủ!
print(tk.xem_so_du()) # Số dư hiện tại: 700,000đ
# Cố truy cập trực tiếp sẽ không được như ý muốn:
# print(tk.__so_du) # AttributeError!
Nhờ đóng gói, không ai có thể tự ý gán tk.__so_du = -999999999 để "hack" tài khoản — mọi thay đổi số dư đều phải đi qua các phương thức có kiểm tra logic hợp lệ.
7.4 Getter và Setter
7.4.1 Getter/Setter là gì?
Getter và Setter là hai loại phương thức đặc biệt dùng để đọc (get) và ghi (set) giá trị của thuộc tính private/protected, thay vì cho phép truy cập trực tiếp.
Liên hệ thực tế: Hãy nghĩ về chiếc đồng hồ điện tử. Bạn không thể tự ý thò tay vào mạch điện để chỉnh giờ — bạn phải bấm nút chỉnh giờ (setter) và nhìn màn hình hiển thị (getter). Nhà sản xuất kiểm soát quy trình chỉnh giờ để đảm bảo bạn không nhập giờ sai (ví dụ: giờ 25, phút 61).
7.4.2 Tại sao cần Getter/Setter?
Khi thuộc tính là public, ai cũng có thể gán bất kỳ giá trị nào — kể cả giá trị vô lý:
class HocSinh:
def __init__(self, ten, tuoi):
self.ten = ten
self.tuoi = tuoi # public — không kiểm soát
hs = HocSinh("An", 16)
hs.tuoi = -5 # tuổi âm?! Python không ngăn cản
hs.tuoi = 99999 # tuổi 99999?! vẫn được
Với getter/setter, bạn có thể kiểm tra dữ liệu trước khi cho phép đọc/ghi:
class HocSinh:
def __init__(self, ten, tuoi):
self.ten = ten
self.__tuoi = tuoi # private — giấu đi
# GETTER — phương thức để ĐỌC giá trị
def get_tuoi(self):
return self.__tuoi
# SETTER — phương thức để GHI giá trị (có kiểm tra)
def set_tuoi(self, tuoi_moi):
if tuoi_moi < 0 or tuoi_moi > 150:
print("Tuổi không hợp lệ!")
return
self.__tuoi = tuoi_moi
hs = HocSinh("An", 16)
print(hs.get_tuoi()) # 16
hs.set_tuoi(17) # hợp lệ
print(hs.get_tuoi()) # 17
hs.set_tuoi(-5) # Tuổi không hợp lệ!
print(hs.get_tuoi()) # 17 — giá trị không thay đổi
7.4.3 Cách truyền thống vs. Cách Pythonic
Cách viết get_tuoi() / set_tuoi() ở trên là cách truyền thống (giống Java, C++). Nó hoạt động tốt, nhưng cú pháp gọi hơi rườm rà:
# Cách truyền thống — phải gọi method
print(hs.get_tuoi()) # đọc
hs.set_tuoi(17) # ghi
# Mong muốn — gọi như thuộc tính bình thường
print(hs.tuoi) # đọc
hs.tuoi = 17 # ghi
Python cung cấp decorator @property để bạn vừa kiểm soát được dữ liệu (như getter/setter), vừa gọi tự nhiên như thuộc tính bình thường. Đây là cách Pythonic — được ưa chuộng trong cộng đồng Python.
7.4.4 @property — Getter/Setter kiểu Pythonic
@property biến một phương thức thành thuộc tính ảo — bên ngoài gọi như thuộc tính, bên trong thực chất là hàm có kiểm tra logic.
class HocSinh:
def __init__(self, ten, tuoi):
self.ten = ten
self.__tuoi = tuoi # thuộc tính private thật sự
@property
def tuoi(self):
"""GETTER — được gọi khi đọc: hs.tuoi"""
return self.__tuoi
@tuoi.setter
def tuoi(self, tuoi_moi):
"""SETTER — được gọi khi ghi: hs.tuoi = 17"""
if not isinstance(tuoi_moi, int):
print("Tuổi phải là số nguyên!")
return
if tuoi_moi < 0 or tuoi_moi > 150:
print("Tuổi không hợp lệ!")
return
self.__tuoi = tuoi_moi
hs = HocSinh("An", 16)
# Đọc — Python tự động gọi @property (getter)
print(hs.tuoi) # 16
# Ghi hợp lệ — Python tự động gọi @tuoi.setter
hs.tuoi = 17
print(hs.tuoi) # 17
# Ghi không hợp lệ — setter ngăn chặn
hs.tuoi = -5 # Tuổi không hợp lệ!
hs.tuoi = "abc" # Tuổi phải là số nguyên!
print(hs.tuoi) # 17 — giá trị vẫn an toàn
Phân tích cú pháp @property:
| Thành phần | Vai trò | Kích hoạt khi nào |
|---|---|---|
@property | Getter — định nghĩa cách ĐỌC giá trị | print(hs.tuoi) |
@tuoi.setter | Setter — định nghĩa cách GHI giá trị | hs.tuoi = 17 |
@tuoi.deleter | Deleter — định nghĩa cách XÓA giá trị (ít dùng) | del hs.tuoi |
Lưu ý: Tên phương thức trong
@property,@tuoi.settervà@tuoi.deleterphải giống nhau (ở đây đều làtuoi). Đây là tên "thuộc tính ảo" mà bên ngoài sẽ dùng.
7.4.5 Ví dụ nâng cao: Property chỉ đọc (read-only)
Nếu bạn chỉ viết @property mà không viết @setter, thuộc tính đó sẽ trở thành chỉ đọc — không thể gán giá trị từ bên ngoài.
class HinhTron:
def __init__(self, ban_kinh):
self.__ban_kinh = ban_kinh
@property
def ban_kinh(self):
return self.__ban_kinh
@ban_kinh.setter
def ban_kinh(self, gia_tri_moi):
if gia_tri_moi <= 0:
print("Bán kính phải lớn hơn 0!")
return
self.__ban_kinh = gia_tri_moi
@property
def dien_tich(self):
"""Thuộc tính tính toán — chỉ đọc, không có setter"""
return 3.14159 * self.__ban_kinh ** 2
@property
def chu_vi(self):
"""Thuộc tính tính toán — chỉ đọc"""
return 2 * 3.14159 * self.__ban_kinh
ht = HinhTron(5)
print(ht.ban_kinh) # 5
print(ht.dien_tich) # 78.53975
print(ht.chu_vi) # 31.4159
ht.ban_kinh = 10 # hợp lệ — có setter
print(ht.dien_tich) # 314.159 — tự động cập nhật!
# ht.dien_tich = 100 # AttributeError — không có setter, chỉ đọc!
Điểm hay:
dien_tichvàchu_viluôn tự động tính lại dựa trênban_kinhhiện tại — không bao giờ bị "lệch" dữ liệu. Đây là lợi ích lớn của property so với thuộc tính thông thường.
7.4.6 Ví dụ thực tế: Sản phẩm có giá và thuế
class SanPham:
def __init__(self, ten, gia):
self.ten = ten
self.__gia = gia
@property
def gia(self):
return self.__gia
@gia.setter
def gia(self, gia_moi):
if gia_moi < 0:
print("Giá không thể âm!")
else:
self.__gia = gia_moi
@property
def gia_sau_thue(self):
"""Tự động tính giá sau thuế VAT 10%"""
return self.__gia * 1.1
def __str__(self):
return f"{self.ten}: {self.gia:,.0f}đ (sau thuế: {self.gia_sau_thue:,.0f}đ)"
sp = SanPham("Áo thun", 150000)
print(sp) # Áo thun: 150,000đ (sau thuế: 165,000đ)
sp.gia = 200000 # gọi setter — hợp lệ
print(sp) # Áo thun: 200,000đ (sau thuế: 220,000đ)
sp.gia = -50000 # Giá không thể âm!
print(sp.gia) # 200000 — giá trị không thay đổi
7.4.7 Tổng kết Getter/Setter
| Cách tiếp cận | Ưu điểm | Nhược điểm | Khi nào dùng |
|---|---|---|---|
Thuộc tính public (self.x) | Đơn giản, nhanh | Không kiểm soát được | Dữ liệu không nhạy cảm |
Getter/Setter truyền thống (get_x(), set_x()) | Kiểm soát tốt | Cú pháp rườm rà | Khi làm việc với Java/C++ developers |
@property (Pythonic) | Kiểm soát + cú pháp tự nhiên | Cần hiểu decorator | ✅ Khuyến khích dùng trong Python |
Quy tắc vàng: Trong Python, hãy bắt đầu với thuộc tính public (
self.x). Khi cần thêm logic kiểm tra, hãy chuyển sang@property— code bên ngoài không cần thay đổi vì cú pháp truy cập vẫn giống hệt thuộc tính bình thường.
8. Trừu tượng hóa (Abstraction)
8.1 Ý tưởng
Trừu tượng hóa là việc định nghĩa một "khung sườn" bắt buộc mà các class con phải tuân theo, nhưng không quan tâm chi tiết cài đặt bên trong ra sao.
Liên hệ thực tế: Ổ cắm điện là một "chuẩn giao tiếp trừu tượng" — bất kỳ thiết bị nào (quạt, tivi, sạc điện thoại) muốn dùng điện đều phải có phích cắm đúng chuẩn. Ổ cắm không quan tâm bên trong thiết bị hoạt động thế nào, chỉ quan tâm nó "cắm vào là chạy".
8.2 Cài đặt với module abc
from abc import ABC, abstractmethod
class HinhHoc(ABC): # Lớp trừu tượng — không thể tạo object trực tiếp
@abstractmethod
def dien_tich(self):
pass # bắt buộc class con phải cài đặt phương thức này
@abstractmethod
def chu_vi(self):
pass
class HinhVuong(HinhHoc):
def __init__(self, canh):
self.canh = canh
def dien_tich(self):
return self.canh ** 2
def chu_vi(self):
return self.canh * 4
# hh = HinhHoc() # Lỗi! Không thể khởi tạo class trừu tượng
hv = HinhVuong(5)
print(hv.dien_tich()) # 25
print(hv.chu_vi()) # 20
Nếu HinhVuong quên cài đặt chu_vi(), Python sẽ báo lỗi ngay khi bạn cố tạo object — giúp đảm bảo mọi class con đều tuân thủ "hợp đồng" mà class trừu tượng đặt ra.
9. Magic Methods
9.1 Magic Method là gì?
Magic Methods (còn gọi là Dunder Methods — "double underscore") là các phương thức đặc biệt có dạng __ten__. Chúng cho phép bạn định nghĩa cách một object phản ứng với các toán tử, hàm tích hợp và cú pháp đặc biệt của Python như +, -, ==, print(), len(), []...
Liên hệ thực tế: Giống như khi bạn dạy một người nước ngoài hiểu ngôn ngữ Việt Nam — bạn "dạy" object của mình cách hiểu các phép toán và câu lệnh quen thuộc của Python. Thay vì gọi
san_pham.tinh_tong(), bạn có thể viết tự nhiên hơn:gio_hang_1 + gio_hang_2.
Tại sao cần Magic Methods?
Khi không có magic methods, code trở nên cồng kềnh và không tự nhiên:
# Không dùng magic methods — rườm rà
tong_gia = gio_hang.tinh_tong_gia()
print(gio_hang.mo_ta())
so_luong = gio_hang.dem_san_pham()
# Dùng magic methods — tự nhiên, giống Python built-in
tong_gia = sum(gio_hang)
print(gio_hang)
so_luong = len(gio_hang)
9.2 Phân loại Magic Methods
| Nhóm | Mục đích | Ví dụ |
|---|---|---|
| Biểu diễn | Kiểm soát cách hiển thị object | __str__, __repr__ |
| Toán học | Định nghĩa phép toán số học | __add__, __sub__, __mul__ |
| So sánh | Định nghĩa phép so sánh | __eq__, __lt__, __gt__ |
| Container | Làm object hoạt động như danh sách/từ điển | __len__, __getitem__, __contains__ |
| Vòng đời | Kiểm soát tạo/hủy object | __init__, __del__ |
| Context Manager | Dùng với with | __enter__, __exit__ |
9.3 Nhóm Biểu diễn: __str__ và __repr__
Hai magic method này kiểm soát cách object được hiển thị dưới dạng văn bản:
__str__: dành cho người dùng — thông báo dễ đọc, thân thiện.__repr__: dành cho lập trình viên — thông tin kỹ thuật, dùng để debug.
class SanPham:
def __init__(self, ten, gia, so_luong):
self.ten = ten
self.gia = gia
self.so_luong = so_luong
def __str__(self):
# Được gọi khi print(object) hoặc str(object) hoặc f"{object}"
return f"[{self.ten}] — {self.gia:,}đ (còn {self.so_luong} cái)"
def __repr__(self):
# Được gọi trong REPL, khi debug, hoặc repr(object)
return f"SanPham(ten='{self.ten}', gia={self.gia}, so_luong={self.so_luong})"
sp = SanPham("Áo thun", 150_000, 20)
print(sp) # [Áo thun] — 150,000đ (còn 20 cái) <- __str__
print(repr(sp)) # SanPham(ten='Áo thun', gia=150000, so_luong=20) <- __repr__
# Khi không có __str__, Python tự động dùng __repr__
# Khi không có cả hai: <__main__.SanPham object at 0x...>
Quy tắc vàng: Luôn viết
__repr__vì nó là fallback. Viết thêm__str__khi cần thông báo thân thiện với người dùng.
9.4 Nhóm Toán học: __add__, __sub__, __mul__
Ví dụ thực tế: Giỏ hàng mua sắm — cộng hai giỏ hàng lại với nhau.
class GioHang:
def __init__(self, ten_khach):
self.ten_khach = ten_khach
self.san_pham = [] # danh sách (tên, giá)
def them(self, ten, gia):
self.san_pham.append((ten, gia))
def tong_tien(self):
return sum(gia for _, gia in self.san_pham)
def __add__(self, gio_khac):
"""Gộp hai giỏ hàng thành một giỏ mới"""
gio_gop = GioHang(f"{self.ten_khach} & {gio_khac.ten_khach}")
gio_gop.san_pham = self.san_pham + gio_khac.san_pham
return gio_gop
def __str__(self):
danh_sach = "\n".join(f" - {ten}: {gia:,}đ" for ten, gia in self.san_pham)
return f"Giỏ hàng của {self.ten_khach}:\n{danh_sach}\n Tổng: {self.tong_tien():,}đ"
gio_an = GioHang("An")
gio_an.them("Áo thun", 150_000)
gio_an.them("Quần jean", 350_000)
gio_binh = GioHang("Bình")
gio_binh.them("Giày thể thao", 600_000)
# Dùng toán tử + nhờ __add__
gio_chung = gio_an + gio_binh
print(gio_chung)
# Giỏ hàng của An & Bình:
# - Áo thun: 150,000đ
# - Quần jean: 350,000đ
# - Giày thể thao: 600,000đ
# Tổng: 1,100,000đ
9.5 Nhóm So sánh: __eq__, __lt__, __gt__
Ví dụ thực tế: So sánh nhiệt độ giữa hai thành phố.
class NhietDo:
def __init__(self, thanh_pho, do_c):
self.thanh_pho = thanh_pho
self.do_c = do_c # nhiệt độ Celsius
def __eq__(self, khac):
"""== : hai nhiệt độ bằng nhau"""
return self.do_c == khac.do_c
def __lt__(self, khac):
"""< : nhiệt độ này thấp hơn"""
return self.do_c < khac.do_c
def __gt__(self, khac):
"""> : nhiệt độ này cao hơn"""
return self.do_c > khac.do_c
def __le__(self, khac):
"""<= : nhỏ hơn hoặc bằng"""
return self.do_c <= khac.do_c
def __str__(self):
return f"{self.thanh_pho}: {self.do_c}°C"
ha_noi = NhietDo("Hà Nội", 22)
da_nang = NhietDo("Đà Nẵng", 28)
hcm = NhietDo("TP.HCM", 33)
print(ha_noi < da_nang) # True — Hà Nội lạnh hơn Đà Nẵng
print(hcm > da_nang) # True — HCM nóng hơn Đà Nẵng
print(ha_noi == da_nang) # False
# Nhờ __lt__, Python có thể tự sắp xếp!
danh_sach = [hcm, ha_noi, da_nang]
danh_sach_sap_xep = sorted(danh_sach) # gọi __lt__ ngầm
for tp in danh_sach_sap_xep:
print(tp)
# Hà Nội: 22°C
# Đà Nẵng: 28°C
# TP.HCM: 33°C
Mẹo: Khi bạn định nghĩa
__lt__,sorted()vàmin()/max()sẽ tự hoạt động với danh sách chứa các object của bạn.
9.6 Nhóm Container: __len__, __getitem__, __contains__
Những magic method này giúp object của bạn hoạt động giống như list hoặc dict.
Ví dụ thực tế: Danh sách học sinh trong lớp học.
class LopHoc:
def __init__(self, ten_lop):
self.ten_lop = ten_lop
self._hoc_sinh = [] # danh sách tên học sinh
def them_hoc_sinh(self, ten):
self._hoc_sinh.append(ten)
def __len__(self):
"""Gọi khi dùng len(lop)"""
return len(self._hoc_sinh)
def __getitem__(self, chi_so):
"""Gọi khi dùng lop[0], lop[1:3], ..."""
return self._hoc_sinh[chi_so]
def __contains__(self, ten):
"""Gọi khi dùng 'An' in lop"""
return ten in self._hoc_sinh
def __iter__(self):
"""Gọi khi dùng for hs in lop"""
return iter(self._hoc_sinh)
def __str__(self):
return f"Lớp {self.ten_lop} ({len(self)} học sinh)"
lop = LopHoc("12A1")
lop.them_hoc_sinh("An")
lop.them_hoc_sinh("Bình")
lop.them_hoc_sinh("Chi")
lop.them_hoc_sinh("Dung")
print(len(lop)) # 4 <- __len__
print(lop[0]) # An <- __getitem__
print(lop[1:3]) # ['Bình', 'Chi'] <- __getitem__ với slice
print("An" in lop) # True <- __contains__
print("Hoa" in lop) # False
# Duyệt qua lớp như một list bình thường nhờ __iter__
for hoc_sinh in lop:
print(f" - {hoc_sinh}")
# - An
# - Bình
# - Chi
# - Dung
9.7 Context Manager: __enter__ và __exit__
Magic method này cho phép object dùng được với từ khóa with, tự động dọn dẹp tài nguyên khi xong.
Ví dụ thực tế: Quản lý kết nối cơ sở dữ liệu (mô phỏng).
class KetNoiDatabase:
def __init__(self, ten_db):
self.ten_db = ten_db
self.ket_noi = False
def __enter__(self):
"""Được gọi khi bắt đầu khối with"""
print(f"🔌 Đang kết nối đến {self.ten_db}...")
self.ket_noi = True
return self # trả về chính object để dùng trong khối with
def __exit__(self, loai_loi, gia_tri_loi, traceback):
"""Được gọi khi kết thúc khối with (dù có lỗi hay không)"""
self.ket_noi = False
print(f"🔒 Đã đóng kết nối đến {self.ten_db}")
return False # không nuốt lỗi, để lỗi tiếp tục lan ra ngoài
def truy_van(self, lenh_sql):
if not self.ket_noi:
raise RuntimeError("Chưa kết nối!")
print(f" ▶ Thực thi: {lenh_sql}")
# Dùng với từ khóa with — tự động đóng kết nối khi xong
with KetNoiDatabase("csdl_ban_hang.db") as db:
db.truy_van("SELECT * FROM san_pham")
db.truy_van("UPDATE san_pham SET gia = 200000 WHERE id = 1")
# 🔌 Đang kết nối đến csdl_ban_hang.db...
# ▶ Thực thi: SELECT * FROM san_pham
# ▶ Thực thi: UPDATE san_pham SET gia = 200000 WHERE id = 1
# 🔒 Đã đóng kết nối đến csdl_ban_hang.db
# Sau khối with, kết nối đã đóng tự động
# db.truy_van("...") # sẽ báo RuntimeError
Liên hệ thực tế: Giống như khi bạn vào phòng máy lạnh (bật điều hòa), làm việc, rồi ra ngoài (tắt điều hòa) —
withđảm bảo bước "tắt điều hòa" luôn được thực hiện, kể cả khi có sự cố xảy ra bên trong.
9.8 Ví dụ tổng hợp: Class PhanSo
Ví dụ dưới đây kết hợp nhiều magic methods vào một class hoàn chỉnh — Phân số trong toán học.
import math
class PhanSo:
def __init__(self, tu_so, mau_so):
if mau_so == 0:
raise ValueError("Mẫu số không thể bằng 0!")
# Tự động rút gọn phân số
uoc_chung = math.gcd(abs(tu_so), abs(mau_so))
self.tu = tu_so // uoc_chung
self.mau = mau_so // uoc_chung
# Đảm bảo mẫu luôn dương
if self.mau < 0:
self.tu, self.mau = -self.tu, -self.mau
# --- Biểu diễn ---
def __str__(self):
if self.mau == 1:
return str(self.tu)
return f"{self.tu}/{self.mau}"
def __repr__(self):
return f"PhanSo({self.tu}, {self.mau})"
# --- Toán học ---
def __add__(self, khac):
"""a/b + c/d = (a*d + c*b) / (b*d)"""
tu_moi = self.tu * khac.mau + khac.tu * self.mau
mau_moi = self.mau * khac.mau
return PhanSo(tu_moi, mau_moi)
def __sub__(self, khac):
"""a/b - c/d = (a*d - c*b) / (b*d)"""
tu_moi = self.tu * khac.mau - khac.tu * self.mau
mau_moi = self.mau * khac.mau
return PhanSo(tu_moi, mau_moi)
def __mul__(self, khac):
"""a/b * c/d = (a*c) / (b*d)"""
return PhanSo(self.tu * khac.tu, self.mau * khac.mau)
def __truediv__(self, khac):
"""a/b ÷ c/d = (a*d) / (b*c)"""
return PhanSo(self.tu * khac.mau, self.mau * khac.tu)
# --- So sánh ---
def __eq__(self, khac):
return self.tu * khac.mau == khac.tu * self.mau
def __lt__(self, khac):
return self.tu * khac.mau < khac.tu * self.mau
def __float__(self):
"""Chuyển sang số thực khi dùng float(phan_so)"""
return self.tu / self.mau
# --- Sử dụng ---
p1 = PhanSo(1, 2) # 1/2
p2 = PhanSo(1, 3) # 1/3
p3 = PhanSo(2, 4) # tự rút gọn thành 1/2
print(p1) # 1/2
print(p2) # 1/3
print(p1 + p2) # 5/6 (1/2 + 1/3 = 5/6)
print(p1 - p2) # 1/6 (1/2 - 1/3 = 1/6)
print(p1 * p2) # 1/6 (1/2 * 1/3 = 1/6)
print(p1 / p2) # 3/2 (1/2 ÷ 1/3 = 3/2)
print(p1 == p3) # True (1/2 == 2/4, đã rút gọn)
print(p2 < p1) # True (1/3 < 1/2)
# Sắp xếp danh sách phân số — tự động dùng __lt__
ds = [PhanSo(3,4), PhanSo(1,2), PhanSo(2,3)]
print(sorted(ds)) # [1/2, 2/3, 3/4]
print(float(p1)) # 0.5
9.9 Bảng tổng kết Magic Methods
| Magic Method | Kích hoạt khi nào | Ví dụ sử dụng |
|---|---|---|
__init__ | Tạo object | obj = MyClass() |
__str__ | print(obj), str(obj) | Hiển thị thân thiện |
__repr__ | repr(obj), trong REPL | Debug, logging |
__add__ | a + b | Cộng hai object |
__sub__ | a - b | Trừ hai object |
__mul__ | a * b | Nhân hai object |
__truediv__ | a / b | Chia hai object |
__eq__ | a == b | Kiểm tra bằng nhau |
__lt__ | a < b | So sánh nhỏ hơn |
__gt__ | a > b | So sánh lớn hơn |
__len__ | len(obj) | Độ dài/số lượng |
__getitem__ | obj[i] | Truy cập bằng chỉ số |
__contains__ | x in obj | Kiểm tra tồn tại |
__iter__ | for x in obj | Duyệt qua object |
__enter__ | with obj as x: | Mở tài nguyên |
__exit__ | Kết thúc khối with | Đóng tài nguyên |
__float__ | float(obj) | Chuyển sang float |
__bool__ | bool(obj), if obj: | Kiểm tra truthy/falsy |
10. Bài tập tổng hợp cuối bài
Hoàn thành lần lượt các bài tập sau, theo đúng thứ tự kiến thức đã học ở trên. Mỗi bài đều đánh dấu phần liên quan để bạn dễ ôn lại lý thuyết.
-
(Mục 2 – Class & Object) Tạo một class tên
DienThoai(chưa cần thuộc tính gì). Tạo 3 object từ class đó, đặt têndt1,dt2,dt3. In ra kiểu dữ liệu (type) của từng object và kiểm tra xem chúng có phải cùng một object hay không (dùngis). -
(Mục 3 –
__init__) Tạo classSanPhamvới__init__nhận vàoten_san_pham,gia, vàso_luong_ton_kho(mặc định = 0). Trong__init__, in ra thông báo "Sản phẩm [tên] đã được thêm vào kho với giá [giá]đ". Viết thêm phương thứcnhap_them_hang(so_luong)để tăng tồn kho vàban_hang(so_luong)để giảm tồn kho (kiểm tra không cho bán quá số lượng tồn). -
(Mục 4 – Thuộc tính & Phương thức) Tạo class
TaiKhoanMangXaHoivới thuộc tínhten_nguoi_dungvàso_luong_bai_post(mặc định = 0). Viết phương thứcdang_bai()— mỗi lần gọi sẽ tăngso_luong_bai_postlên 1 và in ra thông báo "đã đăng bài, tổng cộng: X bài". Tạo 2 tài khoản khác nhau, gọidang_bai()vài lần và kiểm tra xem số bài của mỗi tài khoản có độc lập với nhau không. -
(Mục 5 – Inheritance) Tạo class cha
PhuongTienvới thuộc tínhtenvà phương thứcdi_chuyen()in ra "[tên] đang di chuyển". Tạo class conXeMayvàMayBaykế thừa từPhuongTien, trong đóMayBayghi đèdi_chuyen()để in "[tên] đang bay trên không". Sau đó tạo classNhanVienBanHangkế thừaNhanVien(ở ví dụ mục 5), có thêmhoa_hongvà ghi đètinh_luong()để cộng thêm hoa hồng. -
(Mục 6 – Polymorphism) Tạo class
ThongBaovới phương thứcgui(). Tạo 3 class con:ThongBaoEmail,ThongBaoSMS,ThongBaoPush, mỗi class ghi đègui()để in ra thông báo khác nhau. Tạo một danh sách chứa cả 3 loại thông báo, dùng vòng lặpforđể gọigui()cho từng cái. -
(Mục 7 – Encapsulation) Tạo class
HopThuDienTu(email) với thuộc tính private__mat_khau. Viết phương thứcdoi_mat_khau(mat_khau_cu, mat_khau_moi)— chỉ đổi được nếumat_khau_cuđúng. Sau đó dùng@propertyđể tạo thuộc tínhtuoicho classNguoiDung, không cho phép gán tuổi âm hoặc lớn hơn 150. -
(Mục 8 – Abstraction) Tạo class trừu tượng
PhuongThucThanhToanvới 2 phương thức trừu tượng:thanh_toan(so_tien)vàhoan_tien(so_tien). Tạo 2 class conThanhToanTheATMvàThanhToanViDienTu, cài đặt đầy đủ 2 phương thức trên theo cách riêng. Thử bỏ sót 1 phương thức ở class con và quan sát lỗi Python báo ra là gì. -
(Mục 9 – Magic Methods) Tạo class
PhanSo(a/b) với__init__(self, tu_so, mau_so). Cài đặt__str__để in phân số dạng "a/b". Cài đặt__add__để cộng 2 phân số (gợi ý: a/b + c/d = (a*d + c*b)/(b*d)). Cài đặt__eq__để so sánh 2 phân số có bằng nhau không (gợi ý: so sánh a*d == c*b).
Bài tập thử thách (tổng hợp toàn bộ kiến thức)
Xây dựng hệ thống quản lý cửa hàng bán đồ điện tử, kết hợp mọi khái niệm đã học:
SanPham— class trừu tượng (dùngabc), có thuộc tínhten,_gia(kiểm soát bằng@property, không cho âm),so_luong; có phương thức trừu tượngmo_ta()và phương thức thườngtong_gia_tri().DienThoai,LapTop— kế thừaSanPham, mỗi class overridemo_ta()theo cách riêng (đa hình).KhoHang— chứa danh sách sản phẩm, cóthem_san_pham(),tong_gia_tri_kho()(cộng dồntong_gia_tri()của mọi sản phẩm — không cần biết là điện thoại hay laptop), vàin_bao_cao()(gọimo_ta()của từng sản phẩm).
Gợi ý khung code:
from abc import ABC, abstractmethod
class SanPham(ABC):
def __init__(self, ten, gia, so_luong):
self.ten = ten
self._gia = gia
self.so_luong = so_luong
@property
def gia(self):
return self._gia
@gia.setter
def gia(self, gia_moi):
if gia_moi < 0:
raise ValueError("Giá không thể âm")
self._gia = gia_moi
def tong_gia_tri(self):
return self._gia * self.so_luong
@abstractmethod
def mo_ta(self):
pass
class DienThoai(SanPham):
def __init__(self, ten, gia, so_luong, dung_luong_pin):
super().__init__(ten, gia, so_luong)
self.dung_luong_pin = dung_luong_pin
def mo_ta(self):
return f"{self.ten} - Pin {self.dung_luong_pin}mAh - {self.gia:,}đ"
# TODO: Bạn hãy tự viết tiếp class LapTop và class KhoHang!
Sau khi hoàn thành khung trên, hãy thử thêm class PhuKien (tai nghe, sạc...) kế thừa SanPham và thêm vào kho — quan sát việc không cần sửa code của KhoHang mà hệ thống vẫn chạy đúng. Đây chính là lợi ích lớn nhất mà OOP mang lại: dễ mở rộng, không phá vỡ code cũ.
Tổng kết
| Khái niệm | Câu hỏi cốt lõi | Từ khóa Python |
|---|---|---|
| Class & Object | Bản thiết kế là gì, sản phẩm là gì? | class, () |
| Attributes & Methods | Đối tượng có gì, làm được gì? | self.x, def |
__init__ | Khởi tạo dữ liệu ban đầu ra sao? | __init__(self, ...) |
| Inheritance | Cái gì kế thừa từ cái gì? | class Con(Cha):, super() |
| Polymorphism | Cùng hành động, khác cách làm? | ghi đè phương thức |
| Encapsulation | Giấu gì, lộ gì? | _, __, @property |
| Getter/Setter | Kiểm soát đọc/ghi dữ liệu ra sao? | @property, @x.setter |
| Abstraction | Bắt buộc phải có gì? | ABC, @abstractmethod |
| Magic Methods | Làm sao "dạy" toán tử cho object? | __str__, __add__, __eq__ |
Chúc bạn học tốt và áp dụng OOP thành công vào các dự án Python thực tế!