บทที่ 8 COM Lifecycle
ทำไมต้องจัดการ COM Lifecycle
COM objects ที่สร้างจาก comtypes ไม่ได้ถูกปล่อยอัตโนมัติ ตาม Python garbage collector
ถ้าไม่ cleanup — COM reference จะค้างอยู่ทำให้:
- ETABS process ค้าง ปิดไม่ได้หลังสคริปต์จบ
- Memory Leak — ใช้ memory เพิ่มขึ้นเรื่อยๆ
- RPC Error ในการรันครั้งต่อไป
%%{init: {
"theme": "base",
"flowchart": {
"curve": "basis",
"htmlLabels": true,
"nodeSpacing": 42,
"rankSpacing": 56,
"padding": 16
}
}}%%
flowchart LR
P["Python<br/>creates COM ref"] --> C["COM<br/>ref count + 1"]
C --> E["ETABS<br/>holds memory"]
E --> D["del obj<br/>+ gc.collect"]
D --> CF["COM<br/>ref count - 1"]
CF --> F["ref = 0<br/>→ ETABS ปล่อย memory"]
วิธี Cleanup ใน Python
Pattern: del + gc.collect()
import gc
# ... ทำงานกับ API ...
# Cleanup — ลำดับสำคัญ: child → parentdel sap_model # ปล่อย SapModel ก่อนdel etabs # แล้วปล่อย ETABSObjectgc.collect() # บังคับ garbage collection ทันทีเปรียบเทียบ: C# vs Python Cleanup
| หลักการ | C# | Python |
|---|---|---|
| ปล่อย COM | Marshal.FinalReleaseComObject(obj) | del obj + gc.collect() |
| RAII Pattern | IDisposable + using block | Context Manager + with |
| Cleanup ลำดับ | child → parent (เหมือนกัน) | child → parent (เหมือนกัน) |
| GC Timing | deterministic (Dispose) | non-deterministic (gc.collect) |
Context Manager: ทางออกที่ดีที่สุด
สร้าง class ที่จัดการ lifecycle อัตโนมัติ:
import comtypes.clientimport gc
class EtabsSession: """จัดการ ETABS COM lifecycle อัตโนมัติ ใช้กับ with statement เพื่อ auto-cleanup """
def __init__(self, mode: str = "attach"): """ Args: mode: "attach" = เชื่อม ETABS ที่เปิดอยู่ "create" = สร้าง ETABS ใหม่ """ self.mode = mode self._etabs = None self._sap_model = None
@property def etabs(self): return self._etabs
@property def sap_model(self): return self._sap_model
def connect(self): """เชื่อม ETABS""" if self.mode == "attach": self._etabs = comtypes.client.GetActiveObject( "CSI.ETABS.API.ETABSObject" ) else: self._etabs = comtypes.client.CreateObject( "CSI.ETABS.API.ETABSObject" ) self._etabs.ApplicationStart()
if self._etabs is None: raise ConnectionError("ไม่สามารถเชื่อม ETABS ได้")
self._sap_model = self._etabs.SapModel
if self._sap_model is None: raise ConnectionError("ไม่สามารถเข้าถึง SapModel ได้")
def close(self): """ปล่อย COM objects ตามลำดับที่ถูกต้อง""" if self._sap_model is not None: del self._sap_model self._sap_model = None if self._etabs is not None: del self._etabs self._etabs = None gc.collect()
# === Context Manager === def __enter__(self): self.connect() return self
def __exit__(self, exc_type, exc_val, exc_tb): self.close() return False # ไม่ suppress exceptionการใช้งาน
# Simple usagewith EtabsSession() as session: model = session.sap_model path = model.GetModelFilename() print(f"📁 Model: {path}")
# ทำงานกับ API...
# ← close() เรียกอัตโนมัติเมื่อออกจาก with# ← ถ้าเกิด exception ก็ยัง cleanup ให้จัดการหลาย COM objects
with EtabsSession() as session: model = session.sap_model
# เก็บ intermediate COM objects ไว้ใน list เพื่อ cleanup com_objects = []
frame_api = model.FrameObj com_objects.append(frame_api)
point_api = model.PointObj com_objects.append(point_api)
# Cleanup intermediate objects for obj in reversed(com_objects): del obj gc.collect()
# session cleanup จะจัดการ etabs + sap_model ให้Best Practices
✅ ทำ
- ใช้
withstatement (EtabsSession) ทุกครั้ง delobjects ตามลำดับ child → parent- เรียก
gc.collect()หลังdelทุกตัว - เก็บ reference เฉพาะที่ต้องใช้ (ไม่สร้าง COM object เกินจำเป็น)
❌ อย่าทำ
- ปล่อยให้ script จบโดยไม่ cleanup → ETABS process ค้าง
- เก็บ COM reference ไว้ใน global variable
- ส่ง COM object ข้าม thread (COM เป็น STA by default)
ตรวจว่า COM ปล่อยสำเร็จ
หลัง script จบ ตรวจใน Task Manager:
- ถ้า ETABS.exe ยังค้างอยู่ (ทั้งที่ปิดหน้าต่างแล้ว) → มี COM leak
- ให้ End Task แล้วตรวจโค้ดว่า
delครบหรือไม่