BattOS

Python En Kullanışlı Decorator - cache

Python’da bazen recursive/özyineleyen fonksiyonlar kullanmamız gerekebiliyor. Örneğin çok basit bir işlem olan n. fibonacci sayısını hesaplayan bir fonksiyon yazalım:

def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

Bu işlemi yapan bir fonksiyon yazdık ve şimdi kullanalım.

def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

def main():
    for i in range(100):
        print(i, fib(i))

if __name__ == "__main__":
    main()

Burada yaptığımız işlem 100. fibonacci sayısına kadar hepsini hesaplayıp ekrana yazdırmak. Bunu yaptığımız yol en doğrusu değil ama. Her seferinde tekrar tekrar fib fonksiyonunu çağırıyoruz ve bu işlem biraz yorucu olabilir. Bu programı çalıştırdığımızda 35. sayıdan sonra artık programın çok yavaş çalıştığını görebiliriz. Çalıştırmak istemiyorsanız time ile çalıştırıp ne kadar sürdüğünü yazmak istedim ama 41den sonra sabırsızlanıp durdurdum. 41. fibonacci sayısında durum şu şekide:

Executed in   92.52 secs    fish           external
   usr time   92.47 secs    0.00 micros   92.47 secs
   sys time    0.02 secs  832.00 micros    0.02 secs

Eğer fib(n) hesaplanırken fib(n-1) ve fib(n-2)nin değeri zaten bilinseydi çok daha hızlı olabilirdi değil mi? Bunu zaten hesaplıyoruz aslında ama hafızada saklamadığımız için tekrar hesaplamamız gerekiyor. Peki hafızada nasıl saklayacağız dersek bunu Python’da yapmanın çok basit bir yolu var: cache decorator’ı. Built-in functools kütüphanesi içerisindeki cache fonksiyonu ile fonksiyonla yapılan hesaplamaların sonucu geçici olarak hafızada saklanır ve böylece aynı işlemi tekrar tekrar yapmamız gerekmez. Kullanımı şu şekilde:

from functools import cache

@cache
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

def main():
    for i in range(100):
        print(i, fib(i))

if __name__ == "__main__":
    main()

Şimdi çalıştırdığımızda görürüz ki anında bitiyor ve istediğmiz sonucu veriyor. 100. fibonacci sayısına kadar hesaplayan bu programı time ile çalıştırdığımda şu çıktıyı aldım:

Executed in   35.35 millis    fish           external
   usr time   27.23 millis  363.00 micros   26.87 millis
   sys time    8.22 millis  258.00 micros    7.96 millis

Tabii ki time iyi bir benchmark aracı değil ama genel bir fikir verebilir. Ayrıca buradaki hız farkı da zaten bir benchmark aracı olmadan çok kolay anlaşılabilir.

Daha iyi bir decorator daha var functools kütüphanesinde: lru_cache. Cache’in bir başka veriyonu olan lru_cache‘nin açılımı ‘Least Recently Used Cache’dir. Bunla tüm sonuçları değil sadece belli sayıdaki son sonuçları saklayabiliriz böylece hafızada daha az yer kaplamış oluruz. Kullanımı şu şekilde:

from functools import lru_cache

@lru_cache(maxsize=3)
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

def main():
    for i in range(100):
        print(i, fib(i))

if __name__ == "__main__":
    main()

Burada @lru_cache(maxsize=3) ile sadece son hesaplanan 3 sonucu saklamak istediğimizi söylüyoruz. Böylece hafızada daha az yer kaplıyoruz ve hala ilk halinden kat kat daha hızlı çalışıyor.

İleri okuma için: Real Python - Caching in Python Using the LRU Cache Strategy

Kerem Ü