Cơ bản

Class và OOP trong Python: __init__, self, phương thức, kế thừa và dataclass

Cập nhật: · Skillaria.Top

OOP trong Python xây dựng trên class: class mô tả các trường và phương thức, còn đối tượng (instance) được tạo bằng cách gọi class; __init__ gán giá trị cho các trường, còn self trỏ tới đối tượng hiện tại. Kế thừa cho phép dùng lại hành vi của lớp cha và ghi đè một phần phương thức, còn decorator @dataclass sinh sẵn __init__, __repr__ và __eq__ cho các class chủ yếu lưu dữ liệu.

Class trong Python giống như một bản thiết kế: nó mô tả đối tượng lưu những dữ liệu gì và làm được những việc gì với chúng. Đối tượng (instance) là sản phẩm cụ thể được tạo theo bản thiết kế đó. OOP trong Python dựa trên ba thứ: class với các phương thức, kế thừa, và các phương thức đặc biệt như __init__ và __repr__. Dưới đây là toàn bộ chủ đề, theo một ví dụ xuyên suốt — quản lý chi tiêu.

Vì sao cần class khi đã có dict

Dữ liệu có thể lưu trong dict: {"title": "Cà phê", "amount": 250}. Vấn đề là dict không biết khóa nào là đúng. Gõ nhầm expense.get("amout", 0) sẽ không báo lỗi — nó trả về số 0, và báo cáo âm thầm sai.

Với đối tượng của class, các trường được khai báo trước. Truy cập một thuộc tính không tồn tại sẽ lỗi ngay với AttributeError, và Python hiện đại còn gợi ý tên đúng. Ngoài ra class gom dữ liệu và thao tác trên dữ liệu vào cùng một chỗ: khoản chi tự tính được số tiền có thuế, báo cáo tự tính tổng.

Cách tạo class trong Python

Class được khai báo bằng từ khóa class, tên theo quy ước PEP 8 viết kiểu CamelCase. Ví dụ class đơn giản nhất:

class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

coffee = Expense("Cà phê", 250)
taxi = Expense("Taxi", 300)
print(coffee.title, coffee.amount)   # Cà phê 250

Lời gọi Expense("Cà phê", 250) tạo một instance mới. coffee và taxi là hai đối tượng khác nhau cùng kiểu, mỗi cái có giá trị trường riêng. Kiểm tra một đối tượng thuộc class nào bằng isinstance(coffee, Expense) — cho True.

Constructor __init__: khởi tạo class

__init__ thường được gọi là constructor của class, nhưng chính xác hơn là bộ khởi tạo. Việc tạo đối tượng do phương thức __new__ đảm nhiệm, còn __init__ được gọi ngay sau đó để đưa các tham số vào các trường. Nó không cần trả về gì (chính xác hơn: phải trả về None).

Trong __init__, rất tiện để kiểm tra dữ liệu đầu vào, để chương trình không bao giờ có đối tượng sai ngay từ đầu:

class Expense:
    def __init__(self, title, amount):
        if amount < 0:
            raise ValueError("Số tiền không thể âm")
        self.title = title
        self.amount = amount

self trong Python là gì

self là tham chiếu tới chính đối tượng mà phương thức đang làm việc. Khi viết coffee.with_tax(), Python thực chất gọi Expense.with_tax(coffee) và truyền đối tượng làm tham số đầu tiên. Vì vậy trong khai báo phương thức, self luôn đứng đầu, còn khi gọi thì không viết.

self không phải từ khóa mà là quy ước: về kỹ thuật tham số có thể đặt tên khác, nhưng không ai làm vậy. Lỗi hay gặp của người mới là quên self trong khai báo phương thức. Khi đó lời gọi sẽ lỗi với TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given.

Phương thức của class và phương thức đặc biệt

Phương thức là hàm được khai báo bên trong class. Nó nhìn thấy các trường của đối tượng thông qua self:

class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    def with_tax(self, rate=0.2):
        return self.amount * (1 + rate)

    def __repr__(self):
        return f"Expense({self.title!r}, {self.amount})"

coffee = Expense("Cà phê", 250)
print(coffee.with_tax())   # 300.0
print(coffee)              # Expense('Cà phê', 250)

Không có __repr__, kết quả in ra sẽ là <__main__.Expense object at 0x7f...> — không có ích gì cho việc gỡ lỗi. Các phương thức có hai dấu gạch dưới ở cả hai đầu được gọi là phương thức đặc biệt (dunder): Python tự gọi chúng. __str__ chịu trách nhiệm hiển thị «đẹp» cho người dùng, __eq__ so sánh bằng ==, __len__ phục vụ len(obj).

@classmethod và @staticmethod

Phương thức thông thường làm việc với instance. Phương thức của class với decorator @classmethod nhận chính class (cls) làm tham số đầu tiên — thường dùng như một constructor thay thế. Phương thức tĩnh @staticmethod không nhận cả instance lẫn class: đó là một hàm thông thường nhưng về mặt logic thuộc về class.

class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    @classmethod
    def from_csv(cls, line):
        title, amount = line.split(";")
        return cls(title, float(amount))

    @staticmethod
    def is_valid_amount(value):
        return value >= 0

e = Expense.from_csv("Cơm trưa;520")
print(e.amount)                    # 520.0
print(Expense.is_valid_amount(-1)) # False

Destructor của class: __del__

Python có phương thức __del__, được gọi trước khi đối tượng bị hủy bởi bộ thu gom rác. Không nên dựa vào nó để giải phóng tài nguyên: thời điểm gọi không được đảm bảo. Với tệp và kết nối, hãy dùng trình quản lý ngữ cảnh with hoặc gọi tường minh phương thức close().

Thuộc tính của class và thuộc tính của instance

Các trường gán qua self.name = ... thuộc về một đối tượng cụ thể. Biến khai báo ngay trong thân class là thuộc tính của class: chúng chung cho mọi instance. Thường dùng cho các hằng số của class.

class Expense:
    CURRENCY = "VND"          # thuộc tính của class, hằng số

    def __init__(self, title, amount):
        self.title = title    # thuộc tính của instance
        self.amount = amount

a = Expense("Cà phê", 250)
b = Expense("Trà", 150)
print(a.CURRENCY, Expense.CURRENCY)   # VND VND

Cạm bẫy nguy hiểm là thuộc tính class có thể thay đổi. Nếu khai báo trong thân class rows = [] và thêm phần tử qua self.rows.append(...), danh sách sẽ được dùng chung cho mọi đối tượng. Các trường có thể thay đổi luôn phải tạo trong __init__.

Xem các trường của đối tượng bằng vars(obj) hoặc obj.__dict__, còn đọc thuộc tính theo tên lưu trong chuỗi bằng getattr(obj, "amount").

Property của class: @property

Property là một phương thức mà từ bên ngoài được đọc như một trường thông thường, không có dấu ngoặc. Điều này tiện cho các giá trị tính toán: tổng của báo cáo không cần lưu riêng và không bao giờ bị quên cập nhật.

class Report:
    def __init__(self):
        self.rows = []

    def add(self, expense):
        self.rows.append(expense)

    @property
    def total(self):
        return sum(e.amount for e in self.rows)

r = Report()
r.add(Expense("Cà phê", 250))
r.add(Expense("Taxi", 300))
print(r.total)   # 550

Nếu cần kiểm soát việc ghi, thêm setter bằng @total.setter. Không có setter, phép gán r.total = 0 sẽ gây AttributeError.

Về đóng gói: Python không có trường private theo nghĩa nghiêm ngặt. Một dấu gạch dưới (_cache) là quy ước «đừng đụng vào từ bên ngoài», còn hai dấu (__secret) kích hoạt name mangling — tên được đổi để trường không vô tình bị lớp con ghi đè.

Kế thừa class trong Python

Kế thừa cho phép tạo class con dựa trên class cha: con nhận mọi trường và phương thức, và có thể ghi đè những phương thức cần thay đổi. Class cha được ghi trong dấu ngoặc.

class MonthlyReport(Report):
    def __init__(self, month):
        super().__init__()        # gọi __init__ của cha
        self.month = month

    def add(self, expense):       # ghi đè phương thức
        print(f"[{self.month}] + {expense}")
        super().add(expense)      # giữ hành vi của cha

m = MonthlyReport("tháng 10")
m.add(Expense("Cà phê", 250))   # [tháng 10] + Expense('Cà phê', 250)
print(m.total)                  # 250 — property được kế thừa

super() trả về một đối tượng trung gian để gọi phương thức của cha. Quên super().__init__() trong __init__ của lớp con là lỗi kinh điển: trường rows không được tạo, và lần add đầu tiên sẽ lỗi AttributeError.

Kiểm tra quan hệ kế thừa bằng issubclass(MonthlyReport, Report). Mọi class trong Python 3 đều kế thừa ngầm từ object — đó là nguồn gốc của __repr__, __eq__ mặc định và các phương thức khác. Python cũng hỗ trợ đa kế thừa (class C(A, B)); thứ tự tìm phương thức được thể hiện qua C.__mro__.

Abstract class trong Python

Abstract class định nghĩa một giao diện: các lớp con bắt buộc phải cài đặt những phương thức nào. Không thể tạo instance của nó. Công cụ cho việc này là module abc:

from abc import ABC, abstractmethod

class Exporter(ABC):
    @abstractmethod
    def export(self, report): ...

class CsvExporter(Exporter):
    def export(self, report):
        return "\n".join(f"{e.title};{e.amount}" for e in report.rows)

Exporter()   # TypeError: Can't instantiate abstract class Exporter ...

Lớp con không cài đặt đủ các phương thức trừu tượng cũng vẫn là trừu tượng, và khi tạo đối tượng sẽ gặp đúng lỗi đó.

Composition thay cho cây kế thừa sâu

Sau khi học kế thừa, người ta dễ muốn xây những chuỗi lớp: khoản chi cơ bản, khoản chi bằng tiền, khoản chi bằng rúp — rồi năm tầng cứ thế tiếp tục. Trong Python người ta hiếm khi viết như vậy. Kế thừa mô tả quan hệ «là một», còn composition mô tả quan hệ «chứa»: báo cáo không phải là một danh sách chi tiêu, nó chứa danh sách đó trong trường rows.

Quy tắc thực tế: nếu chỉ cần dùng một khả năng của đối tượng khác, hãy đặt đối tượng đó vào một trường thay vì kế thừa. Với dữ liệu, một dataclass thường là đủ, còn logic có thể để trong các hàm thông thường.

Dataclass trong Python: class không cần viết code lặp

Với các class chủ yếu lưu dữ liệu, tự viết __init__, __repr__ và __eq__ rất mệt. Decorator @dataclass từ module dataclasses sinh ra chúng từ các chú thích kiểu của trường:

from dataclasses import dataclass, field

@dataclass
class Expense:
    title: str
    amount: float
    tags: list[str] = field(default_factory=list)

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("Số tiền không thể âm")

e = Expense("Cà phê", 250)
print(e)                           # Expense(title='Cà phê', amount=250, tags=[])
print(e == Expense("Cà phê", 250))   # True

Kiểm tra giá trị từng được viết trong __init__ thì giờ chuyển sang __post_init__ — nó được gọi ngay sau bộ khởi tạo do dataclass sinh ra. Giá trị mặc định kiểu thay đổi được chỉ khai báo qua field(default_factory=list): viết tags: list = [] thì dataclass không cho phép và sẽ báo ValueError.

frozen và slots

Tham số frozen=True làm đối tượng bất biến, còn slots=True (từ Python 3.10) tạo __slots__ và tiết kiệm bộ nhớ. Cố thay đổi trường của đối tượng bất biến sẽ gây FrozenInstanceError. Để có đối tượng với giá trị mới, người ta tạo bản sao:

from dataclasses import dataclass, replace

@dataclass(frozen=True, slots=True)
class Expense:
    title: str
    amount: float

e = Expense("Cà phê", 250)
# e.amount = 300   -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2)   # Expense(title='Cà phê', amount=250) Expense(title='Cà phê', amount=300)

Từ Python 3.13, hàm đa năng copy.replace() làm được điều tương tự. Các đối tượng dataclass bất biến có thể đưa vào set và dùng làm khóa dict — chúng có sẵn __hash__.

Có gì mới trong class: Self, @override, __slots__

Để gõ kiểu cho class có hai công cụ hữu ích. typing.Self (Python 3.11, PEP 673) nghĩa là «trả về đối tượng cùng class» — hoạt động cả trong các phương thức được kế thừa. Decorator @override (Python 3.12, PEP 698) đánh dấu phương thức bắt buộc phải ghi đè phương thức của cha: nếu bạn gõ nhầm tên, mypy hoặc pyright sẽ báo không có phương thức gốc nào như vậy.

from typing import Self, override

class Report:
    def __init__(self):
        self.rows = []

    def add(self, e) -> Self:
        self.rows.append(e)
        return self              # có thể gọi nối tiếp

class MonthlyReport(Report):
    @override
    def add(self, e) -> Self:
        return super().add(e)

Trong Python 3.15 đã gỡ một hạn chế hẹp: lớp con của tuple được phép có __slots__ không rỗng, còn __dict__ và __weakref__ có thể khai báo trong __slots__ của bất kỳ class nào. Ở 3.14, khai báo như vậy ở lớp con của tuple gây TypeError: nonempty __slots__ not supported for subtype of 'tuple'.

Lỗi thường gặp khi làm việc với class

LỗiTriệu chứngCách sửa
Thiếu self trong phương thứcTypeError: ... takes 0 positional arguments but 1 was givenTham số đầu tiên của phương thức luôn là self
Trường trong __init__ không có self.Giá trị biến mất sau khi __init__ kết thúcViết self.amount = amount
Danh sách làm thuộc tính classDữ liệu «chảy» giữa các đối tượngTạo danh sách trong __init__ hoặc qua default_factory
Thiếu super().__init__()AttributeError với trường của lớp chaGọi bộ khởi tạo của lớp cha
Gọi property có dấu ngoặcTypeError: 'int' object is not callableĐọc property không có ngoặc: r.total

Tổng kết

OOP trong Python bắt đầu từ điều đơn giản: class mô tả trường và phương thức, __init__ điền giá trị vào trường, còn self trỏ tới đối tượng hiện tại. Kế thừa và super() cho phép mở rộng hành vi, abstract class định nghĩa giao diện, còn @property tạo các trường tính toán. Với class chủ yếu chứa dữ liệu, hãy dùng @dataclass, và với cấu trúc phức tạp, ưu tiên composition hơn là cây kế thừa sâu. Chủ đề này được thực hành trong bài giảng khóa học Python qua ví dụ trình theo dõi chi tiêu.

Câu hỏi thường gặp

Class khác đối tượng trong Python ở điểm nào?

Class là mô tả kiểu: nó có những trường và phương thức nào. Đối tượng (instance) được tạo bằng cách gọi class, ví dụ Expense("Cà phê", 250), và lưu những giá trị cụ thể của các trường.

Có bắt buộc phải viết __init__ không?

Không. Nếu không khai báo __init__, class dùng bản kế thừa từ object, và nó không nhận tham số nào. Với dataclass, decorator sẽ tự sinh __init__.

__str__ khác __repr__ ở điểm nào?

__repr__ là biểu diễn không nhập nhằng dành cho lập trình viên và trình gỡ lỗi; __str__ là cách hiển thị dễ đọc cho người dùng khi dùng print và f-string. Nếu không có __str__, Python dùng __repr__.

Khi nào chọn dataclass, khi nào chọn class thường?

Dataclass phù hợp khi class chủ yếu lưu dữ liệu và cần sẵn __init__, __repr__ cùng khả năng so sánh. Class thường tiện hơn khi logic khởi tạo phức tạp hoặc hành vi quan trọng hơn dữ liệu.

Python có trường private không?

Không có tính private nghiêm ngặt. Một dấu gạch dưới ở đầu tên là quy ước «không dành cho dùng bên ngoài», còn hai dấu gạch dưới kích hoạt name mangling, bảo vệ trường khỏi việc bị ghi đè ngẫu nhiên trong lớp con.

Học chủ đề này qua video

Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Python 3.15: от новичка до профи”.

Đến khóa học