นากกายชี้กระดานโน้ตซ้อนชั้นบนโต๊ะมิ้นต์

Python lru_cache: จำผลคำนวณซ้ำให้ฟังก์ชันบริสุทธิ์เร็วขึ้น

น้องกายEngineering

เคยเรียกฟังก์ชันเดิมด้วยอาร์กิวเมนต์เดิมวนไปวนมาจนช้าไหม ผมเคย จนได้ลอง functools.lru_cache

ตามเอกสารมาตรฐาน lru_cache คือตัวตกแต่งที่เก็บผลลัพธ์ล่าสุดแบบ LRU (Least Recently Used — เตะรายการที่ไม่ได้ใช้มานานที่สุดเมื่อแคชเต็ม) เหมาะกับฟังก์ชันที่ผลขึ้นกับอินพุตอย่างเดียวและไม่มีผลข้างเคียง

ใช้เมื่อไหร่

เหมาะกับคำนวณซ้ำ ๆ เช่นแปลงพิกัด แตกพาร์สเล็ก ๆ หรือฟังก์ชันช่วยที่ถูกเรียกบ่อยในลูป ไม่เหมาะกับฟังก์ชันที่อ่านเวลาปัจจุบัน สุ่มค่า หรือเขียนไฟล์ เพราะแคชจะเก็บคำตอบเก่าไว้

อาร์กิวเมนต์ต้องแฮชได้ เช่น ตัวเลข สตริง ทูเปิลของค่าที่แฮชได้ อย่าส่งลิสต์หรือดิกต์ตรง ๆ

import unittest
from functools import lru_cache


@lru_cache(maxsize=32)
def slow_square(n: int) -> int:
    slow_square.calls += 1
    return n * n


slow_square.calls = 0


class LruCacheTests(unittest.TestCase):
    def setUp(self):
        slow_square.cache_clear()
        slow_square.calls = 0

    def test_repeated_args_hit_cache(self):
        self.assertEqual(slow_square(3), 9)
        self.assertEqual(slow_square(3), 9)
        self.assertEqual(slow_square.calls, 1)

    def test_different_args_compute_again(self):
        slow_square(2)
        slow_square(4)
        self.assertEqual(slow_square.calls, 2)
        self.assertEqual(slow_square.cache_info().hits, 0)


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

cache_info() ช่วยดู hits/misses และ cache_clear() ล้างเมื่อข้อมูลต้นทางเปลี่ยน

จุดพลาดที่พบบ่อย

อย่าแคชฟังก์ชันที่ขึ้นกับสถานะภายนอกโดยไม่รู้ตัว จำกัด maxsize ให้เหมาะกับหน่วยความจำ และระวังการแคชค่าใหญ่เกินจำเป็น ถ้าต้องการแคชไม่จำกัดขนาด เอกสารมี cache = lru_cache(maxsize=None) แต่ต้องรับผิดชอบหน่วยความจำเอง

Recap

  • ใช้ lru_cache กับฟังก์ชันบริสุทธิ์ที่ถูกเรียกซ้ำ
  • ตรวจว่าอาร์กิวเมนต์แฮชได้
  • ดู cache_info ในเทสต์หรือดีบัก
  • ล้างแคชเมื่ออินพุตโลกภายนอกเปลี่ยน