Lập trình hướng đối tượng: class và object trong Python

Lập trình hướng đối tượng giúp bạn gom dữ liệu và các thao tác trên dữ liệu đó vào cùng một chỗ. Với chương trình lớn, cách tổ chức này giúp code dễ hiểu và dễ mở rộng hơn nhiều.

Class đầu tiên

class NhanVien:
    def __init__(self, ho_ten, luong):
        self.ho_ten = ho_ten
        self.luong = luong

    def tang_luong(self, phan_tram):
        self.luong *= (1 + phan_tram / 100)
        return self.luong

    def __str__(self):
        return f"{self.ho_ten} - {self.luong:,.0f}đ"

nv = NhanVien("Nguyễn Văn A", 15_000_000)
nv.tang_luong(10)
print(nv)      # Nguyễn Văn A - 16,500,000đ

__init__ chạy khi tạo đối tượng mới. self là chính đối tượng đó — Python truyền nó tự động, bạn chỉ cần khai báo.

Vì sao cần __str__ và __repr__

class Diem:
    def __init__(self, x, y):
        self.x, self.y = x, y

d = Diem(3, 4)
print(d)      # <__main__.Diem object at 0x7f...>  - vô dụng

Thêm hai phương thức này:

class Diem:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):
        return f"Diem(x={self.x}, y={self.y})"   # cho lập trình viên

    def __str__(self):
        return f"({self.x}, {self.y})"            # cho người dùng cuối

__repr__ là thứ hiện ra khi bạn gõ tên biến trong trình thông dịch, hoặc khi in một danh sách chứa đối tượng. Nếu chỉ viết một trong hai, hãy viết __repr__ — Python sẽ dùng nó thay cho __str__ khi thiếu.

Thuộc tính của class và của đối tượng

class TaiKhoan:
    lai_suat = 0.05          # dùng chung cho mọi đối tượng
    so_tai_khoan = 0

    def __init__(self, chu_so_huu):
        self.chu_so_huu = chu_so_huu     # riêng từng đối tượng
        TaiKhoan.so_tai_khoan += 1

a = TaiKhoan("An")
b = TaiKhoan("Bình")
print(TaiKhoan.so_tai_khoan)    # 2

Cái bẫy dùng list làm thuộc tính class

class Gio:
    mat_hang = []            # SAI - dùng chung cho tất cả

g1, g2 = Gio(), Gio()
g1.mat_hang.append("sách")
print(g2.mat_hang)           # ['sách'] - giỏ của người khác cũng có!

Danh sách này được tạo một lần lúc định nghĩa class, nên mọi đối tượng dùng chung. Cách đúng là khởi tạo trong __init__:

class Gio:
    def __init__(self):
        self.mat_hang = []   # mỗi đối tượng một danh sách riêng

property: kiểm soát việc đọc ghi

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):
        if gia_tri <= 0:
            raise ValueError("Bán kính phải lớn hơn 0")
        self._ban_kinh = gia_tri

    @property
    def dien_tich(self):
        return 3.14159 * self._ban_kinh ** 2

h = HinhTron(5)
print(h.dien_tich)      # 78.54 - gọi như thuộc tính
h.ban_kinh = -1         # ValueError

Python không có thuộc tính riêng tư thật sự. Dấu gạch dưới đầu tên là quy ước: “đây là chuyện nội bộ, đừng động vào từ bên ngoài”.

Phương thức của class và phương thức tĩnh

class NgayThang:
    def __init__(self, ngay, thang, nam):
        self.ngay, self.thang, self.nam = ngay, thang, nam

    @classmethod
    def tu_chuoi(cls, s):
        ngay, thang, nam = map(int, s.split("/"))
        return cls(ngay, thang, nam)     # cách tạo đối tượng thứ hai

    @staticmethod
    def la_nam_nhuan(nam):
        return nam % 4 == 0 and (nam % 100 != 0 or nam % 400 == 0)

d = NgayThang.tu_chuoi("15/03/2026")
print(NgayThang.la_nam_nhuan(2026))     # False

classmethod thường dùng làm “hàm khởi tạo thay thế” — tạo đối tượng từ định dạng dữ liệu khác. staticmethod là hàm bình thường nhưng đặt trong class vì liên quan về mặt logic.

dataclass: bớt code lặp

Với những class chỉ để chứa dữ liệu, Python có sẵn công cụ:

from dataclasses import dataclass, field

@dataclass
class SanPham:
    ten: str
    gia: float
    so_luong: int = 0
    the: list[str] = field(default_factory=list)

    @property
    def thanh_tien(self):
        return self.gia * self.so_luong

sp = SanPham("Bàn phím", 450_000, 2)
print(sp)                # SanPham(ten='Bàn phím', gia=450000, ...)
print(sp.thanh_tien)     # 900000

Bạn được __init__, __repr____eq__ miễn phí. Chú ý field(default_factory=list) — đây chính là cách tránh cái bẫy dùng chung danh sách nói ở trên.

Khi nào không cần class

Không phải bài toán nào cũng cần hướng đối tượng. Nếu class của bạn chỉ có __init__ và một phương thức duy nhất, nhiều khả năng một hàm là đủ:

# Thừa
class BoTinhThue:
    def __init__(self, thue_suat):
        self.thue_suat = thue_suat
    def tinh(self, so_tien):
        return so_tien * self.thue_suat

# Đủ rồi
def tinh_thue(so_tien, thue_suat=0.1):
    return so_tien * thue_suat

Class phát huy giá trị khi bạn cần giữ trạng thái qua nhiều lần gọi, hoặc khi có nhiều biến thể cùng chia sẻ một giao diện chung.