งูต้นฉบับหยิบบัตรจากช่องคะแนนและเวลาแล้วจัดเรียงลงถาดบนโต๊ะสีครีม

Python itemgetter: เขียน sort key หลายช่องให้สั้นและเห็นลำดับชัด

พี่โชว์Engineering

เมื่อ sort list ของ tuple หรือ dict ตามหลายช่อง เรามักเขียน lambda row: (row["team"], row["score"]) ซ้ำหลายแห่ง operator.itemgetter สร้าง callable ที่หยิบ key ตามลำดับที่ระบุ ทำให้บรรทัด sort เน้นกติกาข้อมูลแทนกลไกของ lambda ครับ

from operator import itemgetter
import unittest


def leaderboard(rows: list[dict[str, object]]) -> list[dict[str, object]]:
    # คะแนนมาก่อน; ถ้าเท่ากัน เวลาน้อยกว่าชนะ
    normalized = [
        {**row, "score_key": -int(row["score"])}
        for row in rows
    ]
    normalized.sort(key=itemgetter("score_key", "elapsed_ms"))
    return [{k: v for k, v in row.items() if k != "score_key"} for row in normalized]


class LeaderboardTests(unittest.TestCase):
    def test_score_descending_then_time_ascending(self):
        rows = [
            {"name": "A", "score": 8, "elapsed_ms": 90},
            {"name": "B", "score": 10, "elapsed_ms": 120},
            {"name": "C", "score": 10, "elapsed_ms": 80},
        ]
        self.assertEqual(
            [row["name"] for row in leaderboard(rows)],
            ["C", "B", "A"],
        )


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

ตัวอย่างสร้าง key ช่วยเพราะ Python มี reverse ค่าเดียวสำหรับทุกช่อง ถ้าต้องการคะแนนมากไปน้อย แต่เวลาน้อยไปมาก การเปลี่ยนคะแนนเป็นค่าติดลบทำให้ tuple key เดินทิศเดียวกันทั้งหมด สำหรับข้อมูล domain จริง ควรสร้าง record ใหม่หรือใช้ dataclass แทนเติม key ชั่วคราวลง dict ต้นฉบับ

itemgetter จะโยน KeyError หรือ IndexError ถ้าช่องขาด นั่นเหมาะเมื่อ schema บังคับครบ แต่ถ้าข้อมูลภายนอกมีช่องว่าง ให้ normalize และ validate ก่อน sort ไม่ควรซ่อนความไม่ครบด้วย default แบบเงียบ ๆ ส่วน object ที่ใช้ attribute ให้เลือก attrgetter เพื่อบอกชนิดการเข้าถึงให้ตรง

Recap

  • ใช้ itemgetter เมื่อหยิบ key หรือ index ตรง ๆ
  • วางลำดับฟิลด์ตาม priority ของการ sort
  • normalize ทิศทางเมื่อแต่ละฟิลด์เรียงคนละแบบ
  • validate missing field ก่อนเข้า sort boundary