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

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.

Quên đóng giữ luôn resource của process bên ngoài
Pythonprocesskết nốimởMySQL / OSgiữ resourceclose()giải phóng cả hai ✅Python(chạy xong)kết nốivẫn mởMySQL / OSvẫn giữresourceFD cạn /giới hạnkết nối ❌
File và DB trao resource cho OS hoặc process khác ngoài Python. Nếu Python không gọi 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.

Luồng thực thi của with X() as y:
with X() as y:vào__enter__được gọigiá trị trả vềđi vào ythân củablock__exit__dọn dẹpreturnrời
Khi vào, __enter__ chạy và giá trị trả về của nó đi vào biến đặt sau as. Khi rời — bình thường hoặc qua exception — __exit__ luôn được gọi để dọn dẹp.

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.

Viết cùng DatabaseManager từ mẫu và điều khiển nó với with.

① Định nghĩa class DatabaseManager: và gán self.db_name = db_nameself.connection = None trong __init__(self, db_name).

② Định nghĩa __enter__(self). In f"Connecting to {self.db_name}", đặt self.connection = f"connection_to_{self.db_name}", sau đó return self.connection.

③ Định nghĩa __exit__(self, exc_type, exc_val, traceback). In f"Disconnecting from {self.db_name}", đặt self.connection = None, và cuối cùng return False.

④ Bên trong with DatabaseManager("user_data_db") as conn:, in f"active connection: {conn}" rồi print("inserting data").

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

Python Editor

Chạy code để xem đầu ra

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 exceptionexc_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.

Giá trị cụ thể được trao cho 3 tham số của __exit__
chuyện gì xảyra trong withexc_typeexc_valtracebackrời bình thường(không exception)NoneNoneNoneraiseValueError("invalid")<class'ValueError'>ValueError('invalid')<tracebackobject>
Rời bình thường trao None × 3. Exception trao bộ ba «class / instance / traceback». Một raise ValueError("invalid") cụ thể làm nội dung tangible.
Rời bình thường vs rời do exception trong __exit__
rời bình thườngtừ withexc_type =None, v.v.__exit__chỉ dọn dẹprời blockwithexceptiontrong withexc_type / val/ traceback__exit__log + dọn dẹpreturn False-> lan ra
Khi block raise, thông tin của exception được đóng gói vào ba tham số của __exit__. Giá trị trả về chọn nuốt (True) hay lan lại (False).

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__.

Sửa DatabaseManager từ Thực hành 1 để xác nhận hai điều: __exit__ chạy ngay cả khi có exception, và ba tham số của nó nhận giá trị.

① Định nghĩa class DatabaseManager: và gán self.db_name = db_name trong __init__(self, db_name).

② Trong __enter__(self), in f"Enter: {self.db_name}"return self (để as bind chính manager).

③ Trong __exit__(self, exc_type, exc_val, traceback), in cả ba tham số từng dòng (print("exc_type:", exc_type) v.v.), sau đó print(f"Exit: {self.db_name}"), và cuối cùng return False.

④ Bên trong block try:, mở with DatabaseManager("shop_db"):, in "start", sau đó raise ValueError("bad inventory data").

⑤ Bắt với except ValueError as e:print(f"caught outside: {e}").

Python Editor

Chạy code để xem đầu ra

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
with tập trung trách nhiệm open/close vào class
Nếu đi với try / finally
  • Bên gọi — phải viết try / finally mỗ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
Câu lệnh with + context manager
  • 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__
Tách «người dùng resource» khỏi «chủ open/close» là giá trị 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.

QUIZ

Kiểm tra kiến thức

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

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 2Khi một exception được raise bên trong block with, mô tả nào về hành vi của __exit__đúng?

Câu 3Nếu __exit__ trả True, chuyện gì xảy ra với exception được raise bên trong block with?