Notes & software courses · Free to learn
Aph's Blog
On this page

Context Manager (with)

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

ใช้และเขียน with เพื่อจัดการ resource (ไฟล์, connection) ให้เปิด-ปิดถูกต้องเสมอ แม้เกิด error

คุณเคยใช้ with open(...) เปิดไฟล์มาแล้ว หัวข้อนี้อธิบายว่ามันทำงานอย่างไร ทำไมสำคัญ และวิธีเขียน context manager ของตัวเอง — เครื่องมือที่การันตีว่า resource ถูกปิดเสมอ ป้องกัน memory/handle leak

ทำไมต้อง with

ถ้าเปิดไฟล์เองแล้วเกิด error ก่อนถึง close() ไฟล์จะค้างเปิด with แก้ปัญหานี้โดยปิดให้อัตโนมัติเมื่อออกจากบล็อก ไม่ว่าจะจบปกติหรือเกิด exception

python
# ❌ เสี่ยง: ถ้า error ก่อน close() ไฟล์ค้างเปิด
f = open("data.txt", encoding="utf-8")
data = f.read()
f.close()

# ✅ with: ปิดให้อัตโนมัติเสมอ แม้เกิด error ในบล็อก
with open("data.txt", encoding="utf-8") as f:
    data = f.read()
# ออกจากบล็อก = ไฟล์ถูกปิดแล้วแน่นอน

เขียน context manager ด้วย class (__enter__/__exit__)

object จะใช้กับ with ได้ ต้องมี 2 method: __enter__ (ทำตอนเข้าบล็อก, ค่าที่ return ไปเป็น as) และ __exit__ (ทำตอนออก แม้มี error)

python
class Timer:
    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self                 # ค่าที่ as จะได้

    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        elapsed = time.perf_counter() - self.start
        print(f"ใช้เวลา {elapsed:.4f} วินาที")
        # return False = ถ้ามี exception ให้โยนต่อ (ปกติ)

with Timer():
    total = sum(range(1000000))
# ใช้เวลา 0.0xx วินาที  (พิมพ์ตอนออกจากบล็อกเสมอ)
__exit__ ทำงานแม้เกิด error

พารามิเตอร์ exc_type, exc_val, exc_tb จะมีค่าถ้าในบล็อกเกิด exception ทำให้ context manager เป็นที่เหมาะสำหรับ "คืน resource" (ปิดไฟล์, ปิด connection, ปลด lock) ที่ต้องเกิดขึ้นไม่ว่าอะไรจะพัง

วิธีลัด: contextlib.contextmanager

เขียน class เต็มยุ่งยาก ใช้ decorator @contextmanager กับ generator แทนได้ โค้ดก่อน yield = __enter__, หลัง yield = __exit__

python
from contextlib import contextmanager
import time

@contextmanager
def timer():
    start = time.perf_counter()
    try:
        yield                       # ตรงนี้คือ "ในบล็อก with"
    finally:
        elapsed = time.perf_counter() - start
        print(f"ใช้เวลา {elapsed:.4f} วินาที")

with timer():
    total = sum(range(1000000))
ทำไมต้อง try/finally

ใส่ส่วนคืน resource ไว้ใน finally เพื่อให้ทำงานแม้บล็อกเกิด error — ถ้าไม่ใส่ finally แล้วโค้ดในบล็อก raise การคืน resource จะถูกข้าม กลายเป็น leak

สรุปหัวข้อนี้

  • with การันตีว่า resource ถูกคืน/ปิดเสมอ แม้เกิด error
  • class context manager ต้องมี __enter__ และ __exit__
  • ค่าที่ __enter__ คืน = ตัวที่รับด้วย as
  • วิธีลัด: @contextmanager กับ generator (try/finally + yield)
แบบฝึกหัด

1) เขียน context manager ด้วย class ที่ print "เข้า" ตอนเริ่มและ "ออก" ตอนจบ 2) เขียน timer ด้วย @contextmanager 3) ลองให้เกิด error ในบล็อก with แล้วสังเกตว่า __exit__/finally ยังทำงาน 4) เขียน context manager ที่เปิด-ปิดไฟล์ (จำลอง) พร้อม print สถานะ