งูต้นฉบับกำลังจัดก้อนหินสามก้อนตามความสูงบนทางลาดสีเขียวอ่อน

Python total_ordering: ลด comparison boilerplate แต่เก็บกฎลำดับไว้จุดเดียว

พี่โชว์Engineering

value object ที่ต้อง sort มักเขียน __lt__, __le__, __gt__ และ __ge__ ซ้ำจนกฎเริ่มไม่ตรงกันครับ functools.total_ordering ให้เราประกาศ __eq__ กับ comparison หลักเพียงหนึ่งตัว แล้วเติมวิธีที่เหลือจากกฎเดียวกัน

เหมือนวางทางลาดเพียงเส้นเดียว: เมื่อรู้ว่าก้อนไหนอยู่ก่อนและก้อนไหนเท่ากัน เราอนุมานความสัมพันธ์อื่นได้ ไม่ต้องสร้างไม้บรรทัดสี่อันที่อาจตั้งศูนย์ไม่ตรงกัน

from functools import total_ordering
import unittest


@total_ordering
class Priority:
    def __init__(self, rank: int):
        self.rank = rank

    def __eq__(self, other):
        if not isinstance(other, Priority):
            return NotImplemented
        return self.rank == other.rank

    def __lt__(self, other):
        if not isinstance(other, Priority):
            return NotImplemented
        return self.rank < other.rank


class OrderingTests(unittest.TestCase):
    def test_all_relations_come_from_one_rule(self):
        low, high = Priority(1), Priority(3)
        self.assertTrue(low < high)
        self.assertTrue(high > low)
        self.assertTrue(low <= Priority(1))
        self.assertTrue(high >= Priority(3))

    def test_unrelated_type_is_not_silently_ordered(self):
        with self.assertRaises(TypeError):
            _ = Priority(1) < 1


if __name__ == "__main__":
    unittest.main()

NotImplemented ไม่ใช่ False

เมื่อชนิดอีกฝั่งเปรียบเทียบกันไม่ได้ ควรคืน NotImplemented เพื่อให้ Python ลอง reflected operation หรือยก TypeError อย่างถูกต้อง การคืน False จะซ่อนความผิดพลาดและอาจทำให้ทั้ง a < b กับ a > b เป็นเท็จโดยไม่มีคำอธิบาย

เอกสารเตือนว่า decorator เพิ่ม stack frame และวิธีที่สร้างอาจช้ากว่าการเขียนครบเอง งานทั่วไปแลกความเร็วเล็กน้อยกับกฎที่รวมศูนย์ได้คุ้ม แต่ hot loop ที่พิสูจน์จาก profiler แล้วค่อยเขียน comparator ครบและเพิ่ม property tests ตรวจ transitivity อีกชั้น ที่สำคัญคือ equality และ ordering ต้องใช้ identity เดียวกัน หาก __eq__ ดูทั้ง rank กับชื่อ แต่ __lt__ ดู rank อย่างเดียว sorted container อาจมี behavior ชวนสับสน

Recap

  • นิยาม __eq__ และ comparator หนึ่งตัวจาก key เดียวกัน
  • คืน NotImplemented เมื่อชนิดเปรียบเทียบกันไม่ได้
  • ทดสอบ equal, less, greater และชนิดที่ไม่เกี่ยวข้อง
  • optimize comparator เองเมื่อ profiler ชี้ว่าจำเป็นจริง