Python 5 — OOP & Clean Code

13 thg 7, 2026 4 lượt xem
#python
#clean-code
#oop
#type-hints

Mở đầu

Lập trình hướng đối tượng (OOP — Object-Oriented Programming) là cách tổ chức code xoay quanh các "đối tượng" — những thực thể gói chung dữ liệu (thuộc tính) và hành vi (phương thức). Thay vì rải các biến và hàm rời rạc khắp file, ta nhóm chúng lại theo khái niệm có ý nghĩa trong miền bài toán: một User, một BankAccount, một Order.

Bài này đi sâu từ con số không (class là gì) tới mức "hero" (dunder methods, dataclass, type hints, SOLID, công cụ chuẩn hoá code). Mọi ví dụ đều bám sát tình huống thực tế chứ không chỉ là Animal hay Dog cho có.


Class & instance: viên gạch đầu tiên

Một class là bản thiết kế (blueprint). Một instance (thể hi) là một vật thể cụ thể được đúc ra từ bản thiết kế đó. Lớp Car mô tả "xe nói chung"; còn my_car = Car(...) là chiếc xe cụ thể của bạn.

class BankAccount:
    """Một tài khoản ngân hàng đơn giản."""

    def __init__(self, owner: str, balance: float = 0.0) -> None:
        self.owner = owner        # thuộc tính của instance
        self.balance = balance

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Số tiền nạp phải dương")
        self.balance += amount

    def withdraw(self, amount: float) -> None:
        if amount > self.balance:
            raise ValueError("Số dư không đủ")
        self.balance -= amount


acc = BankAccount("Lan", 100_000)   # tạo instance
acc.deposit(50_000)
acc.withdraw(30_000)
print(acc.balance)                   # 120000

__init__self

  • __init__constructor — chạy ngay sau khi instance được tạo, dùng để khởi tạo trạng thái ban đầu. Nó không "tạo" object (việc đó do __new__ lo), mà chỉ thiết lập thuộc tính.
  • self là tham chiếu tới chính instance đang gọi phương thức. Nó luôn là tham số đầu tiên của phương thức instance. Khi viết acc.deposit(50_000), Python tự dịch thành BankAccount.deposit(acc, 50_000)acc chính là self.

Thuộc tính class vs thuộc tính instance

Thuộc tính khai báo trong thân class (ngoài phương thức) được chia sẻ cho mọi instance; còn self.x = ... là riêng từng instance.

class BankAccount:
    bank_name = "NCB"          # thuộc tính class — dùng chung

    def __init__(self, owner: str) -> None:
        self.owner = owner     # thuộc tính instance — riêng

a = BankAccount("Lan")
b = BankAccount("Hùng")
print(a.bank_name, b.bank_name)   # NCB NCB (chung)
BankAccount.bank_name = "NCB Plus"
print(a.bank_name)                # NCB Plus (đổi 1 lần, áp cho tất cả)

Lưu ý: đừng dùng thuộc tính class kiểu mutable (list, dict) làm giá trị mặc định chung, vì mọi instance sẽ vô tình chia sẻ cùng một đối tượng.


Kế thừa & super()

Kế thừa (inheritance) cho phép một lớp con tái sử dụng và mở rộng lớp cha. Quan hệ "là một" (is-a): SavingsAccount là một BankAccount.

class SavingsAccount(BankAccount):
    def __init__(self, owner: str, balance: float = 0.0,
                 rate: float = 0.05) -> None:
        super().__init__(owner, balance)   # gọi __init__ của lớp cha
        self.rate = rate

    def add_interest(self) -> None:
        self.balance += self.balance * self.rate


s = SavingsAccount("Mai", 1_000_000, rate=0.06)
s.add_interest()
print(s.balance)        # 1060000.0
s.deposit(40_000)       # kế thừa được từ BankAccount

super() trả về một "proxy" tới lớp cha, giúp gọi phương thức cha mà không hard-code tên lớp. Điều này quan trọng khi có đa kế thừa: super() tuân theo MRO (Method Resolution Order) — thứ tự Python tìm phương thức theo thuật toán C3. Bạn có thể xem MRO bằng SavingsAccount.__mro__.

Ưu tiên composition (chứa đối tượng khác làm thuộc tính) hơn kế thừa sâu khi có thể. Kế thừa nhiều tầng dễ trở nên cứng nhắc và khó suy luận.


Đa hình & duck typing

Đa hình (polymorphism): cùng một lời gọi phương thức cho ra hành vi khác nhau tuỳ kiểu đối tượng. Python theo triết lý duck typing: "nếu nó kêu quạc như vịt thì coi nó là vịt" — không quan tâm kiểu cụ thể, chỉ cần có phương thức cần dùng.

class Circle:
    def __init__(self, r: float) -> None:
        self.r = r
    def area(self) -> float:
        return 3.14159 * self.r ** 2

class Rectangle:
    def __init__(self, w: float, h: float) -> None:
        self.w, self.h = w, h
    def area(self) -> float:
        return self.w * self.h


def total_area(shapes) -> float:
    # Không cần Circle/Rectangle kế thừa cùng cha — chỉ cần có .area()
    return sum(shape.area() for shape in shapes)

print(total_area([Circle(2), Rectangle(3, 4)]))   # 24.566...

Khi muốn quy ước một "giao diện" chung rõ ràng, dùng abc.ABC (abstract base class) với @abstractmethod để buộc lớp con phải hiện thực phương thức.

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

Encapsulation: public / _protected / __private

Python không có từ khoá private cứng như Java. Thay vào đó là quy ước:

Cú phápÝ nghĩa quy ước
namepublic — dùng tự do
_name"protected" — nội bộ, đừng đụng từ ngoài
__name"private" — bị name mangling thành _Class__name
class Wallet:
    def __init__(self) -> None:
        self.currency = "VND"     # public
        self._fee = 0.01          # nội bộ (chỉ là quy ước)
        self.__pin = "1234"       # bị đổi tên thành _Wallet__pin

w = Wallet()
print(w.currency)          # OK
print(w._fee)              # chạy được nhưng "không nên" đụng
# print(w.__pin)           # AttributeError
print(w._Wallet__pin)      # "1234" — name mangling, không phải bảo mật thật

Mục tiêu của encapsulation là che giấu chi tiết và tạo ranh giới rõ ràng giữa "API công khai" và "ruột bên trong", chứ không phải bảo mật. Hai gạch dưới __ chủ yếu để tránh xung đột tên khi kế thừa.


Dunder / magic methods

Các phương thức dạng __xxx__ ("double underscore" = dunder) cho phép object của bạn tích hợp với cú pháp và hàm dựng sẵn của Python.

class Money:
    def __init__(self, amount: int, currency: str = "VND") -> None:
        self.amount = amount
        self.currency = currency

    def __repr__(self) -> str:
        # Dành cho lập trình viên: nên tái tạo được object
        return f"Money(amount={self.amount}, currency={self.currency!r})"

    def __str__(self) -> str:
        # Dành cho người dùng cuối: dễ đọc
        return f"{self.amount:,} {self.currency}"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Money):
            return NotImplemented
        return (self.amount, self.currency) == (other.amount, other.currency)

    def __add__(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError("Không cộng được hai loại tiền khác nhau")
        return Money(self.amount + other.amount, self.currency)


m = Money(1_000_000)
print(str(m))                 # 1,000,000 VND     (__str__)
print(repr(m))                # Money(amount=1000000, currency='VND')
print(Money(50) == Money(50)) # True              (__eq__)
print(Money(50) + Money(70))  # Money(...120...)  (__add__)

Một số dunder hay dùng:

  • __repr__ / __str__: biểu diễn chuỗi (debug vs hiển thị).
  • __eq__, __lt__, __hash__: so sánh và dùng làm key trong set/dict.
  • __len__: hỗ trợ len(obj).
  • __getitem__, __setitem__: hỗ trợ obj[key].
  • __iter__, __next__: hỗ trợ vòng lặp for.
  • __enter__, __exit__: hỗ trợ with (context manager).
class Playlist:
    def __init__(self, songs: list[str]) -> None:
        self._songs = songs
    def __len__(self) -> int:
        return len(self._songs)
    def __getitem__(self, i: int) -> str:
        return self._songs[i]

p = Playlist(["A", "B", "C"])
print(len(p))      # 3
print(p[1])        # B
for song in p:     # __getitem__ cũng đủ để lặp
    print(song)

@property: getter/setter kiểu Pythonic

@property biến một phương thức thành thuộc tính "ảo", cho phép kiểm soát truy cập mà giao diện vẫn trông như thuộc tính thường (không cần get_x() / set_x()).

class Temperature:
    def __init__(self, celsius: float = 0.0) -> None:
        self._celsius = celsius

    @property
    def celsius(self) -> float:
        return self._celsius

    @celsius.setter
    def celsius(self, value: float) -> None:
        if value < -273.15:
            raise ValueError("Dưới độ không tuyệt đối")
        self._celsius = value

    @property
    def fahrenheit(self) -> float:        # thuộc tính chỉ-đọc, tính toán
        return self._celsius * 9 / 5 + 32


t = Temperature(25)
print(t.fahrenheit)   # 77.0 — gọi như thuộc tính, không phải t.fahrenheit()
t.celsius = 30        # đi qua setter, được kiểm tra
# t.celsius = -300    # ValueError

@classmethod & @staticmethod

  • @classmethod: nhận cls (chính lớp) thay vì self. Thường dùng làm factory (constructor thay thế).
  • @staticmethod: không nhận self lẫn cls — chỉ là một hàm tiện ích đặt trong namespace của lớp.
from datetime import date

class User:
    def __init__(self, name: str, birth_year: int) -> None:
        self.name = name
        self.birth_year = birth_year

    @classmethod
    def from_age(cls, name: str, age: int) -> "User":
        # factory: tạo User từ tuổi thay vì năm sinh
        return cls(name, date.today().year - age)

    @staticmethod
    def is_adult(age: int) -> bool:
        return age >= 18


u = User.from_age("Bình", 20)   # classmethod -> cls = User
print(u.birth_year)             # 2006
print(User.is_adult(16))        # False — không cần instance

@classmethod tôn trọng kế thừa: nếu Admin(User) gọi Admin.from_age(...) thì clsAdmin, tạo đúng kiểu con.


dataclass: bớt code lặp

Với những lớp chủ yếu để chứa dữ liệu, viết tay __init__, __repr__, __eq__ rất nhàm. @dataclass sinh tự động chúng cho bạn.

from dataclasses import dataclass, field

@dataclass
class Product:
    name: str
    price: float
    tags: list[str] = field(default_factory=list)   # tránh mutable mặc định
    in_stock: bool = True

p = Product("Bàn phím", 590_000, tags=["gear"])
print(p)                       # Product(name='Bàn phím', price=590000.0, ...)
print(p == Product("Bàn phím", 590_000, tags=["gear"]))   # True

Các tuỳ chọn hữu ích:

  • @dataclass(frozen=True): tạo object bất biến (immutable) và hashable, dùng được làm key trong dict/set.
  • @dataclass(order=True): tự sinh __lt__, __le__... để so sánh và sắp xếp.
  • field(default_factory=list): dùng cho giá trị mặc định mutable (KHÔNG viết tags: list = [] vì sẽ chia sẻ chung một list).

Type hints & mypy

Type hints (PEP 484) thêm chú thích kiểu cho biến, tham số, giá trị trả về. Python không ép kiểu lúc chạy — hint chủ yếu phục vụ con người, IDE và công cụ kiểm tra tĩnh như mypy / pyright.

from typing import Optional

def find_user(users: dict[int, str], uid: int) -> Optional[str]:
    # Optional[str] == str | None
    return users.get(uid)

def total(prices: list[float]) -> float:
    return sum(prices)

Cú pháp hiện đại (Python 3.10+):

  • list[int], dict[str, int] thay cho List, Dict từ typing.
  • int | None thay cho Optional[int].
  • from __future__ import annotations cho phép dùng tên lớp chưa định nghĩa.

Chạy kiểm tra: mypy app.py. mypy bắt được lỗi như truyền str vào chỗ cần int, gọi phương thức không tồn tại, hoặc quên xử lý None — trước khi code chạy.


Sơ đồ lớp


Nguyên tắc clean code

Code chạy đúng là chưa đủ — nó còn phải dễ đọc, dễ sửa. Code được đọc nhiều hơn được viết.

Đặt tên có ý nghĩa

# Tệ
def calc(d, r):
    return d * (1 + r)

# Tốt
def apply_interest(principal: float, rate: float) -> float:
    return principal * (1 + rate)

Tên biến/hàm nên nói rõ ý định. Tránh viết tắt khó hiểu, tránh data, tmp, x cho biến quan trọng.

Hàm nhỏ, làm một việc

Mỗi hàm nên làm một việc ở một mức trừu tượng. Nếu phải cuộn màn hình mới đọc hết, hoặc tên hàm có chữ "and", hãy tách ra. Hàm nhỏ dễ đặt tên, dễ test, dễ tái dùng.

DRY (Don't Repeat Yourself)

Tránh lặp logic. Khi thấy copy-paste, hãy trích thành hàm/lớp dùng chung. (Nhưng đừng trừu tượng hoá quá sớm — hai đoạn trông giống nhau chưa chắc một khái niệm.)

SOLID (giới thiệu)

Năm nguyên tắc thiết kế hướng đối tượng:

  • S — Single Responsibility: mỗi lớp một lý do để thay đổi.
  • O — Open/Closed: mở để mở rộng, đóng để sửa đổi (thêm hành vi qua kế thừa/composition, không sửa code cũ).
  • L — Liskov Substitution: lớp con phải thay thế được lớp cha mà không phá vỡ hành vi mong đợi.
  • I — Interface Segregation: nhiều giao diện nhỏ tốt hơn một giao diện to cồng kềnh.
  • D — Dependency Inversion: phụ thuộc vào trừu tượng, không vào hiện thực cụ thể (truyền dependency vào thay vì tự tạo bên trong).

Ở mức này, chỉ cần ghi nhớ tinh thần: mỗi thứ một trách nhiệm rõ ràng, và phụ thuộc lỏng lẻo (loose coupling).


PEP8 & công cụ: ruff, black

PEP8 là hướng dẫn phong cách chính thức của Python: thụt lề 4 dấu cách, dòng ≤ 79 (nhiều team dùng 88/100), snake_case cho hàm/biến, PascalCase cho lớp, UPPER_CASE cho hằng, khoảng trắng quanh toán tử...

Thay vì nhớ và canh tay, dùng công cụ:

  • blackcode formatter "không thoả hiệp": tự định dạng lại file theo một phong cách thống nhất. Hết tranh cãi về dấu cách.
  • rufflinter (và formatter) cực nhanh viết bằng Rust: bắt lỗi style, import thừa, biến không dùng, và nhiều quy tắc khác. Có thể thay cho flake8 + isort + nhiều plugin.
pip install ruff black

black app.py        # tự định dạng
ruff check app.py   # tìm lỗi/style
ruff check --fix .  # tự sửa những lỗi sửa được

Tích hợp vào pre-commit hoặc CI để mọi commit đều sạch và đồng nhất, không phụ thuộc thói quen từng người.


Tóm tắt

  • Class là blueprint, instance là vật thể cụ thể; __init__ khởi tạo trạng thái, self trỏ tới chính instance.
  • Kế thừa tái sử dụng lớp cha, super() gọi phương thức cha theo MRO; ưu tiên composition khi kế thừa trở nên sâu.
  • Đa hình & duck typing: chỉ cần object có phương thức cần dùng; abc dùng khi muốn giao diện rõ ràng.
  • Encapsulation trong Python là quy ước (_, __ + name mangling), không phải bảo mật cứng.
  • Dunder methods tích hợp object với cú pháp Python (__str__, __repr__, __eq__, __len__...); @property cho getter/setter Pythonic; @classmethod/@staticmethod cho factory và hàm tiện ích.
  • dataclass giảm code lặp cho lớp chứa dữ liệu; type hints + mypy bắt lỗi sớm; clean code (tên rõ, hàm nhỏ, DRY, SOLID) + PEP8 với ruff/black giữ code dễ đọc và đồng nhất.

Tự kiểm tra

  1. Phân biệt __str____repr__ — mỗi cái dành cho ai và khi nào được gọi?
  2. Vì sao không nên dùng tags: list = [] làm tham số mặc định, và dataclass giải quyết bằng cách nào?
  3. super().__init__() trong lớp con có tác dụng gì? Điều gì xảy ra nếu bỏ quên?
  4. Giải thích duck typing bằng ví dụ hàm total_area ở trên — vì sao không cần CircleRectangle chung lớp cha?
  5. @classmethod khác @staticmethod ở điểm nào? Cho một tình huống dùng @classmethod làm factory.
  6. Nêu hai khác biệt giữa ruffblack về vai trò của chúng.

Đọc tiếp

Python 6 — Lỗi, File & Logging

Bài viết liên quan

Vì sao Python là ngôn ngữ số một của data engineer: vai trò trong pipeline (ingest/transform/orchestrate), hệ sinh thái thư viện (pandas/polars/pyarrow/sqlalchemy), quản lý môi trường (venv/uv/poetry), và khi nào dùng Python vs SQL/Spark.

13 thg 7, 2026 4

Định nghĩa hàm, tham số, *args/**kwargs, lambda, module/package, pip và virtualenv.

13 thg 7, 2026 4

Kết nối CSDL với SQLAlchemy, gọi REST API với requests/httpx, và lập trình bất đồng bộ asyncio.

13 thg 7, 2026 4

Exception handling, context manager (with), đọc/ghi file, JSON/CSV và logging đúng cách.

13 thg 7, 2026 3