Câu 1Trong with X() as y:, giá trị bind vào y là giá trị trả về của method nào?
Câu lệnh with và Context Manager — Mở/đóng an toàn với __enter__ / __exit__
Học câu lệnh with và context manager trong Python. Cặp __enter__ / __exit__ để mở/đóng an toàn, và cách tự viết — kèm thực hành.
Lần trước ta đã làm việc với bảo vệ trạng thái nội bộ class. Lần này ta bước ra một lớp ngoài — tới resource bên ngoài sống ngoài process Python (file, kết nối database, network socket, lock) — và xem câu lệnh with cùng context manager xử lý giành và giải phóng chúng an toàn.
Vì sao bạn cần câu lệnh with
Các thao tác như mở file hoặc kết nối database đi kèm nhiệm vụ dọn dẹp: «một khi xong, đóng nó». Quên đóng, và bạn rò rỉ file descriptor, giữ kết nối DB mãi mãi, và để process bên ngoài cũng giữ resource.
close(), bên kia tiếp tục chờ chỉ thị và giữ resource.Bạn có thể viết cùng logic với try / finally, nhưng khi đó mọi tác giả phải nhớ gọi close() trong finally mỗi lần. Khi codebase phát triển hoặc thêm người động vào, ai đó sẽ quên — đó chỉ là thực tế.
Câu lệnh with đóng giành và giải phóng vào một đơn vị cú pháp và tự động hóa chúng. with open("file.txt") as f: là ví dụ kinh điển: file được đảm bảo đóng ngay khi bạn rời block with.
Tự viết Context Manager — __enter__ và __exit__
Object có thể dùng với with được gọi là context manager. Để biến class thành một, chỉ cần triển khai hai special method.
- __enter__(self) — chạy khi bạn vào block with. Giá trị trả về của nó được bind vào biến đặt sau as.
- __exit__(self, exc_type, exc_val, traceback) — chạy khi bạn rời block. Luôn được gọi, bình thường hay qua exception.
Ví dụ kiểu kết nối DB tối thiểu bên dưới (ta không thực sự dùng thư viện DB — bắt chước với chuỗi).
class DatabaseManager:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None # chưa kết nối
def __enter__(self):
print(f"Connecting to {self.db_name}")
self.connection = f"connection_to_{self.db_name}" # code thật: object kết nối thực
return self.connection # giá trị bind vào biến as
def __exit__(self, exc_type, exc_val, traceback):
print(f"Disconnecting from {self.db_name}")
self.connection = None # dọn dẹp
return False # không nuốt exception
with DatabaseManager("user_data_db") as conn:
print(f" active connection: {conn}")
print(" inserting data")
# ↑ khi block này rời, __exit__ chạy
Chạy nó và output xuất hiện theo thứ tự «kết nối → việc trong block → ngắt kết nối». Ngắt kết nối chạy mà không ai gọi rõ ràng — đó là toàn bộ giá trị của with. Lập trình viên được giải phóng khỏi lo lắng đóng kết nối.
Ba tham số của __exit__ — Bắt exception
__exit__ nhận ba tham số: exc_type, exc_val, traceback. Python dùng chúng để báo __exit__ biết liệu có exception xảy ra bên trong block with.
- Rời bình thường — cả ba là None. Chỉ dọn dẹp.
- Rời do exception — exc_type là class exception, exc_val là instance, traceback là object traceback.
Giá trị trả về của __exit__ cũng có ý nghĩa. Trả True nuốt exception — nó không lan ra ngoài block. Trả False / None lan lại sau khi dọn dẹp. Mặc định nên là False (hoặc return không có gì): log hoặc thông báo, nhưng luôn để exception thoát ra.
None × 3. Exception trao bộ ba «class / instance / traceback». Một raise ValueError("invalid") cụ thể làm nội dung tangible.Trả True từ __exit__ giết exception trong im lặng
Nếu __exit__ trả True, exception bên trong with không lan ra. Hấp dẫn, nhưng bên gọi giờ tưởng thao tác thành công — đó là tác dụng phụ nghiêm trọng. Mặc định là False (hoặc không return): log hoặc thông báo nếu muốn, nhưng luôn để exception trồi lên.
Bây giờ thực sự kích hoạt một exception bên trong with và quan sát cái gì tới ba tham số của __exit__.
So với try / finally — Vì sao with thắng
Công việc của context manager có thể làm bằng try / finally. Lý do chọn with thay vào đó là «cặp open/close sống bên trong class». Viết cùng nhiệm vụ theo hai cách làm khác biệt về lượng code bên gọi và độ rõ ràng trở nên hiển nhiên.
# ❌ try / finally — bên gọi viết tay dọn dẹp mỗi lần
db = DatabaseManager("shop_db")
conn = db.open() # method connect tùy chỉnh
try:
use(conn) # việc thật
finally:
db.close() # đừng quên — copy-paste khắp nơi
# ✅ with — open/close sống trong class, bên gọi chỉ làm việc
with DatabaseManager("shop_db") as conn:
use(conn) # không cần finally
- Bên gọi — phải viết
try/finallymỗi lần - Quên — một copy-paste tệ và rò rỉ xuất hiện
- Chi phí thay đổi — bước dọn dẹp thêm có nghĩa sửa mọi nơi gọi
- Bên gọi — một dòng,
with X() as y: - Quên — không thể ở cấp cú pháp (
__exit__luôn chạy) - Chi phí thay đổi — dọn dẹp thêm chỉ sửa
__exit__
with thêm. Khi nơi gọi nhân lên, thay đổi dọn dẹp vẫn ở bên trong một class duy nhất.Dùng with bất cứ nơi nào giành và giải phóng đi cặp
File, kết nối DB, lock, network socket — bất cứ nơi nào bạn «giành resource ở đầu và phải trả ở cuối» — đều là ứng viên cho with. Thư viện chuẩn Python đã expose nhiều cái này dưới dạng context manager: open(), threading.Lock(), sqlite3.connect(), v.v.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Khi một exception được raise bên trong block with, mô tả nào về hành vi của __exit__ là đúng?
Câu 3Nếu __exit__ trả True, chuyện gì xảy ra với exception được raise bên trong block with?