Câu 1Nếu một class C có __init__(self, name), chuyện gì xảy ra khi bạn gọi C() không có đối số?
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
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."
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
Đố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" và "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.
__del__ — Dọn dẹp khi instance biến mất
Dunder method ghép đôi với __init__ là __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.
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__ là 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ạo và xó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 và đố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().
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
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 đề?