Python ExitStack: จัดการ resource จำนวนไม่แน่นอนให้ปิดย้อนลำดับ
with ธรรมดาเหมาะเมื่อรู้จำนวน resource ตอนเขียนโค้ด แต่บางงานเปิดไฟล์ตามรายการผู้ใช้, ล็อกหลายก้อนตามแผน หรือเลือก context manager ตาม configuration จำนวนจึงไม่แน่นอน contextlib.ExitStack ให้เราสะสมทางออกระหว่าง runtime แล้วปิดย้อนลำดับเหมือน with ซ้อนกัน
WHY: acquisition สำเร็จเพียงครึ่งเดียวก็ต้องคืนของ
สมมติต้องเปิด resource A, B และ C หาก B ล้ม A ต้องถูกปิดทันที โค้ดที่เก็บรายการแล้ว cleanup ท้ายฟังก์ชันมักพลาด exception ระหว่างทาง ExitStack ลงทะเบียน __exit__ ทันทีที่ enter_context() สำเร็จ จึงคง invariant ว่า “ทุกสิ่งที่เปิดสำเร็จมีทางปิดแล้ว”
stack ยังรับ callback ธรรมดา เหมาะกับ resource ที่ไม่มี context manager เช่นการคืน token หรือถอน temporary registration แต่ callback ที่ลงทะเบียนด้วย callback() ไม่ได้รับรายละเอียด exception จึงใช้เพื่อ cleanup ไม่ใช่เพื่อ suppress ข้อผิดพลาด
HOW: ตรวจลำดับ LIFO ด้วย unittest
import unittest
from contextlib import ExitStack, contextmanager
@contextmanager
def resource(name, events):
events.append(f"open:{name}")
try:
yield name
finally:
events.append(f"close:{name}")
def use_resources(names):
events = []
with ExitStack() as stack:
opened = [stack.enter_context(resource(name, events)) for name in names]
stack.callback(events.append, "callback:last-registered")
events.append("use:" + ",".join(opened))
return events
class ExitStackTests(unittest.TestCase):
def test_cleanup_is_reverse_registration_order(self):
self.assertEqual(
use_resources(["a", "b"]),
[
"open:a",
"open:b",
"use:a,b",
"callback:last-registered",
"close:b",
"close:a",
],
)
def test_empty_input_is_valid(self):
self.assertEqual(
use_resources([]),
["use:", "callback:last-registered"],
)
if __name__ == "__main__":
unittest.main()
pop_all() ใช้เมื่อ ownership ย้ายจริง
บางครั้งเราต้อง validate การเปิดครบทุกตัวก่อน แล้วส่งสิทธิ์ cleanup ให้ object อื่น pop_all() ย้าย callback ไป stack ใหม่โดยไม่เรียกทันที ใช้ได้เมื่อ ownership contract ชัดเจนเท่านั้น มิฉะนั้น resource จะรั่วเพราะ stack เดิมไม่รับผิดชอบแล้ว
อย่าใช้ ExitStack ซ่อน control flow ที่ซับซ้อน หากมี resource เพียงสองตัวคงที่ with a, b: อ่านง่ายกว่า และอย่าลืมว่า close() เรียก callback แต่ไม่คืน exception information เช่นเดียวกับการออกจาก with ปกติ
Recap
เลือก ExitStack เมื่อจำนวนหรือชนิด resource ทราบตอน runtime ลงทะเบียน cleanup ทันทีหลัง acquisition และทดสอบ LIFO รวมทั้ง input ว่าง งานวันนี้คือลด try/finally ซ้อนกันในฟังก์ชันหนึ่งจุดโดยไม่เปลี่ยน ownership contract ครับ