Python singledispatch: แยกพฤติกรรมตามชนิดโดยไม่สร้าง if ยาว
ฟังก์ชันที่เริ่มจาก if isinstance(...) สองกรณีมักอ่านง่าย แต่เมื่อรองรับสิบชนิด ทุก feature ต้องแก้ก้อนกลางและเสี่ยงกระทบเส้นทางเดิมครับ functools.singledispatch ช่วยคงจุดเรียกเดียว แล้วแยก implementation ตามชนิดของ argument ตัวแรก
WHY: เปิดทางขยายโดยไม่เปิดก้อนกลาง
dispatcher เหมาะกับงาน serialize, render หรือ normalize ที่ความต่างหลักมาจากชนิดข้อมูล เอกสาร Python ระบุว่าถ้าไม่พบชนิดตรง ระบบจะเดิน method resolution order เพื่อหา implementation ที่กว้างกว่า และสุดท้ายใช้ฟังก์ชันฐานสำหรับ object
ข้อดีคือ module อื่นลงทะเบียน handler เพิ่มได้โดยไม่แก้เงื่อนไขเดิม แต่ข้อแลกเปลี่ยนคือ control flow ไม่ได้อยู่หน้าเดียว เราจึงต้องตั้งชื่อจุดลงทะเบียนให้หาเจอและทดสอบ fallback เสมอ
HOW: ลงทะเบียนชนิดกว้างและชนิดเฉพาะ
import unittest
from collections.abc import Mapping
from functools import singledispatch
@singledispatch
def summarize(value):
return f"value:{value}"
@summarize.register
def _(value: int):
return f"integer:{value}"
@summarize.register
def _(value: Mapping):
return "mapping:" + ",".join(sorted(value))
class SummaryTests(unittest.TestCase):
def test_exact_type_uses_registered_handler(self):
self.assertEqual(summarize(7), "integer:7")
def test_abstract_base_class_covers_dict(self):
self.assertEqual(summarize({"b": 2, "a": 1}), "mapping:a,b")
def test_unknown_type_uses_fallback(self):
self.assertEqual(summarize(2.5), "value:2.5")
if __name__ == "__main__":
unittest.main()
การ register Mapping ทำให้ dict และชนิดที่สอดคล้องกันใช้ handler เดียว ไม่ต้องลงทะเบียนทุก concrete class ส่วน int เป็นกรณีเฉพาะที่ชนะ implementation กว้างกว่า
เมื่อไม่ควรใช้
ถ้าการตัดสินใจมาจากสถานะหรือค่าของ object ไม่ใช่ชนิด เช่นยอดเงินเกินวงเงิน ให้ใช้เงื่อนไขตรง ๆ ถ้าแต่ละชนิดควรรู้วิธีทำงานของตัวเองอยู่แล้ว เมธอดปกติหรือ protocol อาจชัดกว่า และถ้าต้อง dispatch หลาย argument singledispatch ไม่ได้แก้โจทย์นั้น
หลีกเลี่ยงการ register กระจัดกระจายตอน import โดยไม่มี test เพราะลำดับ import อาจทำให้คนอ่านไม่รู้ว่า registry สุดท้ายมีอะไร ใช้ dispatch(Type) และ registry ใน diagnostic/test เพื่อยืนยัน wiring ได้
Recap
- ใช้ singledispatch เมื่อพฤติกรรมต่างกันตามชนิด argument แรกจริง ๆ
- เขียน fallback ที่ปลอดภัยและทดสอบชนิดไม่รู้จัก
- ลงทะเบียน abstract base class เมื่อหลายชนิดแชร์สัญญา
- เลี่ยงการใช้แทนเงื่อนไขตามค่า หรือ polymorphism ที่ object ควรถือเอง
พรุ่งนี้เลือก isinstance chain หนึ่งก้อน วาดตารางชนิดกับผลลัพธ์ก่อน แล้วค่อยตัดสินว่า dispatcher ทำให้ boundary ชัดขึ้นหรือไม่ครับ