Python ExitStack.pop_all: โอนสิทธิ์ cleanup เมื่อทุกขั้นเตรียมสำเร็จ
การเปิด resource หลายชิ้นมีช่วงเสี่ยง: ชิ้นแรกสำเร็จแต่ชิ้นถัดไปล้มเหลว เราต้องปิดสิ่งที่เปิดแล้วทั้งหมดครับ ExitStack ลงทะเบียน cleanup ทีละขั้น และ pop_all() โอน callback ทั้งชุดไป stack ใหม่เมื่อ setup สำเร็จครบ ทำให้ transaction ของ resource ชัดเจน
WHY: ฝากกุญแจไว้กับเคาน์เตอร์จนตรวจห้องครบ
เหมือนรับกุญแจหลายห้องแล้วฝากไว้กับเคาน์เตอร์ชั่วคราว ถ้าห้องใดมีปัญหา เคาน์เตอร์คืนกุญแจทั้งหมด แต่เมื่อเราตรวจครบจึงรับพวงกุญแจไปดูแลเอง การโอน ownership ต้องมีเจ้าของใหม่ที่ปิด resource ภายหลังจริง
from contextlib import ExitStack
import unittest
def prepare(names, fail_at=None):
events = []
with ExitStack() as staging:
for name in names:
events.append(f"open:{name}")
staging.callback(events.append, f"close:{name}")
if name == fail_at:
raise RuntimeError(name)
owner = staging.pop_all()
return events, owner
class ExitStackOwnershipTests(unittest.TestCase):
def test_pop_all_defers_cleanup_to_new_owner(self):
events, owner = prepare(["a", "b"])
self.assertEqual(events, ["open:a", "open:b"])
owner.close()
self.assertEqual(events, ["open:a", "open:b", "close:b", "close:a"])
def test_failure_closes_resources_in_reverse_order(self):
with self.assertRaises(RuntimeError):
prepare(["a", "b"], fail_at="b")
if __name__ == "__main__":
unittest.main()
หลังโอนแล้วต้องมี owner ที่มองเห็นได้
pop_all ไม่เรียก cleanup เพียงย้าย callback ออก หากทิ้ง stack ใหม่ resource จะรั่วโดยการออกแบบ ควรคืน owner ออกจาก factory, ผูกกับ object ที่มี close หรือใช้ context manager ชั้นนอก และทำ close ให้ idempotent เท่าที่ resource รองรับ
Callback ทำงานย้อนลำดับเหมือน nested with จึงลงทะเบียนทันทีหลัง acquire อย่ารอท้ายฟังก์ชัน หาก cleanup เองล้มเหลวให้กำหนด log/exception policy ที่ไม่กลบสาเหตุหลักโดยไร้ร่องรอย
Recap
- ลงทะเบียน cleanup ทันทีหลัง acquire
- ใช้
pop_allเมื่อ setup ครบและต้องโอน ownership - คืน owner ที่มีเส้นทาง
closeชัดเจน - ทดสอบทั้ง success และ failure ระหว่างทาง