Học bằng cách đọc theo thứ tự

Constructor __init__ và Destructor __del__

Hướng dẫn thực hành về __init__ và __del__ của Python. Bao quát thuộc tính bắt buộc, giá trị mặc định, và phương thức dọn dẹp được gọi khi del và khi chương trình kết thúc — kèm sơ đồ.

Lần trước bạn đã nắm được căn bản về class và instance, self, và dạng đơn giản nhất của __init__. Lần này ta đi sâu hơn, dùng __init__ để đảm bảo mọi instance được sinh ra ở trạng thái hợp lệ. Ta sẽ bao quát đối số bắt buộc dừng chương trình ngay khi bạn quên, đối số mặc định cho các trường tùy chọn, và destructor đối ứng __del__.

Điều gì xảy ra khi không có __init__

Nếu class của bạn không khởi tạo thuộc tính đúng cách, bạn có thể có những instance thiếu các thuộc tính đó, và ngay khoảnh khắc bạn gọi một phương thức chạm đến chúng, bạn sẽ bị AttributeError đập vào mặt.

Trong đoạn code dưới, User chỉ có phương thức set_name — không có hàm khởi tạo. Gọi display() trước set_name sẽ nổ tung vì self.name chưa tồn tại.

class User:
    def set_name(self, name):
        self.name = name

    def display(self):
        print(self.name)

user = User()
user.display()             # AttributeError: không có thuộc tính name
# self.name chưa tồn tại cho đến khi bạn gọi user.set_name("Minh") trước

Xác nhận rằng gọi một phương thức trước khi khởi tạo thuộc tính của nó sẽ kích hoạt AttributeError. Chạy nó và xem lỗi xảy ra.

Python Editor

Chạy code để xem đầu ra

Dùng __init__ để bắt buộc các thuộc tính

__init__ là phương thức đặc biệt mà Python tự động gọi khi instance được tạo. Nó còn được gọi là constructor. Tên được bao bởi hai dấu gạch dưới là dunder method — quy ước của Python cho các phương thức nó gọi vào những thời điểm nhất định.

Bằng cách khai báo __init__(self, name, email) với tham số bắt buộc, quên truyền một trong hai sẽ dừng chương trình với TypeError. Bạn không bao giờ kết thúc với một User() không hoàn chỉnh, nên mọi code sau đó có thể tin tưởng vào "name và email chắc chắn đã được gán."

Cách __init__ móc vào việc tạo instance
User('Minh', 'minh@x.com')tạo instancerỗnggọi__init__userthuộc tính đã gán
Gọi User("Minh", "minh@x.com") khiến Python tạo một instance rỗng, chạy __init__, điền các thuộc tính, và trả về instance hoàn chỉnh. Quên một đối số và __init__ sẽ ném TypeError.
class User:
    def __init__(self, name, email):    # tham số bắt buộc
        print("__init__ được gọi")
        self.name = name
        self.email = email

    def display(self):
        print(f"{self.name} <{self.email}>")

user = User("Minh", "minh@example.com")  # gán user.name là "Minh" và user.email là "minh@example.com"
# __init__ được gọi
user.display()
# Minh <minh@example.com>

# Quên một đối số sẽ gây ra TypeError
# User()  # → TypeError: missing 2 required positional arguments

Viết __init__ với hai tham số bắt buộc và thử cả việc tạo, in, cũng như chuyện gì xảy ra khi quên một cái.

① Định nghĩa class User:. Bên trong __init__(self, name, email), viết self.name = nameself.email = email.

② Định nghĩa def display(self): in ra f"{self.name} <{self.email}>".

③ Tạo minh = User("Minh", "minh@example.com") và gọi minh.display(). Xác nhận nó in ra Minh <minh@example.com>.

④ Sau đó thử linh = User("Linh")quên email — và xác nhận nó dừng với TypeError (bọc trong try / except để xem thông báo).

(Nếu chạy đúng, phần giải thích sẽ hiện ra.)

Python Editor

Chạy code để xem đầu ra

Đối số mặc định cho các trường tùy chọn

__init__ là một hàm bình thường, nên bạn có thể cho nó đối số mặc định. Viết kiểu age=0, và caller có thể bỏ qua (và nhận giá trị mặc định) hoặc truyền một giá trị. Điều này cho phép bạn chia thiết kế thành "thuộc tính bắt buộc""thuộc tính tùy chọn."

class User:
    def __init__(self, name, email, age=0):   # age là tùy chọn
        self.name = name
        self.email = email
        self.age = age

minh = User("Minh", "minh@example.com")            # bỏ qua age → 0
linh = User("Linh", "linh@example.com", 30)              # age được truyền

print(minh.age)   # 0
print(linh.age)     # 30

Đừng đặt list / dict trực tiếp vào đối số mặc định

Viết một object mutable làm mặc định — như def __init__(self, tags=[]) — sẽ rơi vào một bẫy nổi tiếng: mọi instance đều dùng chung một list. Ta nói chi tiết về điều này trong Đối số hàm và mutability. Đó cũng chính là cái bẫy bên trong __init__, nên cách sửa chuẩn là tags=None cộng với if tags is None: tags = [] bên trong hàm.

Viết một đối số mặc định là list rỗng trực tiếp và xem chuyện gì sai.

① Định nghĩa class User:. Bên trong __init__(self, name, tags=[]), viết self.name = nameself.tags = tags (cố ý đặt tags=[] trực tiếp).

② Tạo minh = User("Minh")linh = User("Linh").

③ Chạy minh.tags.append("vip"), rồi print(minh.tags)print(linh.tags) — xác nhận rằng linh cũng có "vip".

④ Cuối cùng, print(minh.tags is linh.tags) để xác nhận chúng dùng chung một list.

Python Editor

Chạy code để xem đầu ra

__del__ — Dọn dẹp khi instance biến mất

Dunder method ghép đôi với __init____del__ (destructor). Trong khi __init__ chạy khi instance được tạo, __del__ chạy ngay khoảnh khắc instance bị hủy, được Python gọi tự động.

Có hai cách chính khiến hủy xảy ra:

- Bạn xóa nó tường minh bằng del tên_biến

- Bộ nhớ được thu hồi khi chương trình kết thúc (mọi instance còn sót lại bị hủy lần lượt)

Vì trường hợp thứ hai, __del__ chạy khi chương trình kết thúc dù bạn không tự kích hoạt.

Khi __init__ và __del__ chạy
gọi User('Minh')__init__chạyuserđang sốngdel user/ chương trình kết thúc__del__chạybộ nhớđược giải phóngtạosẵn sànghủydọn dẹp
Hàng trên là luồng __init__ — chạy khi instance ra đời, được Python gọi tự động. Hàng dưới là luồng __del__ — chạy khi instance chết, dù bằng del hay khi chương trình kết thúc.

__del__ không chạy khi thực thi trên trình duyệt

Việc thực thi phía trình duyệt của site này được xây trên MicroPython, theo thiết kế không gọi __del__. Các đoạn code mẫu trong phần này dành cho đọc và hiểu hành vi. Chạy chúng trên CPython tại terminal cục bộ nếu bạn muốn thực sự thấy __del__ chạy.

class User:
    def __init__(self, name):
        print(f"đã tạo {name}")
        self.name = name

    def __del__(self):
        print(f"đã hủy {self.name}")

minh = User("Minh")      # đã tạo Minh
linh = User("Linh")          # đã tạo Linh

del minh                  # đã hủy Minh  ← chạy tường minh ở đây
print("sắp thoát")
# sắp thoát
# đã hủy Linh  ← chạy tự động khi chương trình kết thúc

Một pattern dọn dẹp phổ biến — Tự xóa khỏi list

Một cách dùng tiêu biểu của __del__dọn dẹp ghép đôi với việc tạo — như "xóa chính mình khỏi một list mà class đang giữ." Trong ví dụ dưới, class User thêm tên vào class variable users khi tạoxóa nó khi hủy.

class User:
    users = []                         # class variable: users hiện đang sống

    def __init__(self, name):
        self.name = name
        User.users.append(name)        # đăng ký khi tạo

    def __del__(self):
        User.users.remove(self.name)   # xóa khi hủy

minh = User("Minh")
linh = User("Linh")
print(User.users)    # ['Minh', 'Linh']

del minh
print(User.users)    # ['Linh']

Dùng trực tiếp __del__ hiếm gặp trong thực tế

Khi nào __del__ chạy phụ thuộc vào garbage collector của Python, nên bạn không thể dự đoán thời điểm một cách đáng tin cậy. Để giải phóng file hay kết nối mạng một cách xác định, dùng câu lệnh with (context manager) thay vì __del__.

Lần này ta đã bao quát việc dùng __init__ thực tế — bắt buộc thuộc tínhđối số mặc định — và gặp __del__ đối ứng với pattern tiêu biểu của nó. Bạn giờ có thể kiểm soát "vòng đời của một instance từ tạo đến hủy" từ phía Python.

Tiếp theo, ta sẽ đào sâu vào sự thật phía sau self.x = ... mà bạn đã viết mà không nghĩ nhiều. Ta sẽ phân biệt hai loại biến trong một class — class variable và instance variable — và đi qua cái gì thực sự được tạo khi bạn viết self.x = ..., theo dõi tham chiếu bằng id().

QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Nếu một class C__init__(self, name), chuyện gì xảy ra khi bạn gọi C() không có đối số?

Câu 2Thời điểm đúng cho __del__ chạy là gì?

Câu 3Vì sao viết def __init__(self, tags=[]) lại được xem là có vấn đề?