Python built-in metotlar ile önbellekleme nasıl kullanılır

Kader Miyanyedi
8 min readFeb 28, 2023

--

Yeni bir yazıdan herkese selamlar! Yazılım yaşam döngüsü boyunca, uygulamalarımızın hızını arttırmak ve performansını iyileştirmek için çözümler ararız. Bu yazı kapsamında built-in Python önbelleğe alma yöntemleri kullanarak uygulama performansımızı nasıl iyileştiğini inceleyeceğiz.

Yazı boyunca aşağıdaki konulardan bahsedeceğiz:

  1. Cache nedir ve ne zaman kullanmalıyız?
  2. Dictionary veri yapısı kullanılarak önbelleğe alma
  3. Built-in @lru_cache decorator kullanılarak önbelleğe alma
  4. Built-in @cache decorator kullanılarak önbelleğe alma
  5. Built-in @cached_property decorator kullanılarak önbelleğe alma

✨ Cache nedir ve ne zaman kullanmalıyız?

Bir okul sistemi düşünelim. Bir derse her tıkladığımızda dersle ilgili detaylar (ders içerikleri, sınavlar vb.) veri tabanından çekilecek ve bu işlem zaman alacaktır. Binlerce öğrencinin aynı anda aynı derse baktığını düşünürsek işlem maliyetinin artacağını tahmin edebiliriz.
Ders detaylarında sürekli bir değişiklik olmayacağı için bu işlemin gereksiz olduğunu düşünebiliriz. Peki, ders detayının ilk görüntülenmesinden sonra aynı verileri tekrar kullanabilir miyiz?
Temel olarak önbelleğe alma kavramı, verileri kaynağın dışında geçici bir alanda tutmak ve oradan çağırmak anlamına gelir. Bu, verileri kaynaktan almaktan daha hızlı olacaktır. (Aksi durumda cache kullanılması mantıklı değildir.)

Peki önbelleğe alma sistemini ne zaman kullanmalıyız? Genel olarak, önbelleğe alma sistemi en çok şu durumlarda etkilidir:

  • Önbelleğe alınan verilere sıklıkla erişilir ve veri sıklıkla değişmediği durumlar.
  • Verileri önbellekten almak, orijinal kaynaktan almaktan çok daha hızlı olduğu durumlar.
  • Verileri önbellekte depolamanın maliyeti, orijinal kaynaktan almanın maliyetinden daha düşük olduğu durumlar.

❗️❗️Önbelleğe almanın her zaman en iyi çözüm olmadığını ve bazı durumlarda hiç uygun olmayabileceğini unutmamak önemlidir. Örneğin, önbelleğe alınan veriler sürekli değişiyorsa veya çok sık erişilmiyorsa, önbelleğe alma uygulamak için harcanan çabaya değmeyebilir. Ayrıca verilerin sürekli değiştiği bir durumda önbellekleme yapılması kullanıcıya hatalı verilerin gösterilmesine neden olabilir.

Artık önbellek kavramını öğrendiğimize göre Python’da yer alan built-in önbelleğe alma yöntemlerini inceleyebiliriz.

✨ Dictionary veri yapısı kullanılarak önbelleğe alma

Python’da önbelleğe almak için birden çok yöntem bulunmaktadır ve dictionary veri yapısını kullanarak kendi önbellek yapınızı oluşturabilirsiniz. Bir dictionary veri yapısındaki verileri okumak oldukça hızlıdır ve O(n) zaman karmaşıklığına sahiptir.

Bir Fibonacci örneği ile dictionary veri yapısı kullanarak nasıl önbellekleme yapabileceğimizi inceleyelim ve performansını karşılaştıralım.

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

fib_result = fibonacci(25)
print(f"Result: {fib_result}")

Programı herhangi bir önbellek sistemi olmadan çalıştırdığımızda 12 milisaniye sürdüğünü gözlemliyoruz. Dictionary veri yapısı kullanarak bir önbellek sistemi oluşturalım ve programı tekrar çalıştırarak sonucu inceleyelim:

cache = {0:0, 1:1}

def fibonacci(n):
if n in cache:
return cache[n]
result = fibonacci(n-1) + fibonacci(n-2)
cache[n] = result
return result

fib_result = fibonacci(25)
print(f"Result: {fib_result}")

Programın çalışması 0.007 milisaniyede tamamlandı. Önbelleklemenin kullanılmasının daha hızlı olduğunu söyleyebiliriz.

✨ Built-in @lru_cache decorator kullanarak önbelleğe alma

@lru_cache; Python 3.4'ten itibaren kullanılabilen ve functools modülünde yer alan bir decoratordür. Memoizasyon tekniğini kullanır ve aynı girişler için işlevin tekrar yürütülmesini azaltmaya yardımcı olur.

✏️ ️Memoization bir önbelleğe alma tekniğidir ve fonksiyon bir kez çalıştığında sonucu hafızada tutar. İşlevi aynı girişlerle birden çok kez çalıştırmaya gerek kalmadan sonucu bellekten alır ve performansı artırır.

@lru_cache(maxsize=<max_size>, typed=True/False)

@lru_cache decorator optional iki parametreye sahiptir:

  • maxsize: Önbellekte tutulacak maksimum veri sayısını ifade eder. maxsize parametresini None olarak ayarlayabilirsiniz. None olarak ayarlanırsa, önbellek tüm değerleri korur ve süresiz olarak artar(önbellekten hiçbir değer temizlenmez). Çok fazla verinin önbelleğe alınması sorun oluşturabilir.
  • typed: Farklı türlerdeki değişkenler için ayrı bir önbellek tutulup tutulmayacağını belirtir. True olarak ayarlanırsa, farklı değişken türlerini ayrı olarak önbelleğe alır. (str ve int gibi veri türleri, yazılanlar yanlış olsa bile farklı şekilde önbelleğe alınabilir.)

Fibonacci örneğini @lru_cache decorator ile yeniden yazalım ve sonucu görelim.

from functools import lru_cache

@lru_cache(maxsize=32)
def fibonacci(n):
if n < 2:
return n
result = fibonacci(n-1) + fibonacci(n-2)
return result

fib_result = fibonacci(25)
print(f"Result: {fib_result}")

Programın çalışması 0.024 milisaniyede tamamlandı. Önbelleksiz Fibonacci örneğine göre daha hızlı olduğunu söyleyebiliriz.

✏️✏️ @lru_cache decorator ile ilgili notlar:

  • Hesaplanmış bir sonucu aynı girdilerle yeniden kullanmak istediğimizde kullanılmalıdır.
  • Her çağrıda farklı nesneler oluşturan fonksiyonlarda kullanılmamalıdır.
    time() veya random() gibi değişken işlevlerin önbelleğe alınması gerekli değildir.
  • Yalnızca bir python işlemi için çalışır.
  • Özellikle özyinelemeli işlevlerde veya CPU’ya bağlı işlemlerde kullanışlıdır.
  • İşlevi farklı değişken sıralamalarıyla çağırmak, farklı önbellek girişlerine neden olabilir. Örneğin, f(a=1,b=2) ve f(b=2,a=1) iki ayrı önbellek girişine sahip olabilir.

❗❗Sınıf örneklerinin methotlarında @lru_cache kullanırken dikkatli olun

Bir sınıf methotlarında @lru_cache decorator kullanarak önbellek mekanizmasını oluşturabiliriz. Ancak program yaşam döngüsü sona erene kadar, sınıf örneğini referans eden önbellekleme sistemi bellekte yer kaplar. Bellekten hiçbir zaman silinmeyen bu örnekler, memory leaks problemine neden olabilir. Bunu bir örnekle daha iyi inceleyelim.

import functools

class Example:
def __init__(self, number):
self.number = number

@functools.lru_cache(maxsize=None)
def sum_of_squares(self):
return sum([num**2 for num in range(self.number+1)])

start = time.time()
example = Example(1000)
result = example.sum_of_squares()
end = time.time()
print(f"Result: {result} Time: {(end-start)*1000} milliseconds")

start = time.time()
result = example.sum_of_squares()
end = time.time()
print(f"Result: {result} Time: {(end-start)*1000} milliseconds")

Programı çalıştırdığımızda @lru_cache decoratorün beklenildiği gibi çalıştığını ve programı hızlandırdığını görebiliriz. Ancak programı bir python interaktif uçbiriminde incelediğimizde sınıf nesnesi silinse bile bellekte bir önbellek tutulduğunu görebiliriz.

cache_info() metodu ile sınıfa baktığımızda, önbelleğin temizlenene kadar sınıf örneğine bir referans tuttuğunu görürüz. Önbelleği sonsuza kadar tutacak şekilde maxsize=None ayarını yaptığımızı göz önünde bulundurursak, bu durumda bellek dolmaya başlar ve memory leak kaçınılmaz bir sorun haline gelir.

Peki, bu sorunu nasıl çözebiliriz?
Bu sorunu çözmek için önbellek sistemini yerel hale getirmemiz gereklidir. Bu temelde sınıf gövdesinde bir atama işlemi olacak ve bu şekilde örneğin kendisine değil sınıfa bağlı olacaktır. Önbelleğe alınan sınıf örneğine yapılan referans, örnekle birlikte silinecek ve gereksiz bellek alanı kullanılmayacaktır.

class Example:
def __init__(self, number):
self.number = number
self.sum_of_square = functools.lru_cache(maxsize=None)(self._sum_of_squares_uncached)

def _sum_of_squares_uncached(self):
return sum([num**2 for num in range(self.number+1)])

Burada önbelleği manuel olarak temizlememiz gerekli değildir. Garbage collection(çöp toplama) işlemini başlatmak için gc.collect() işlevini çağırmamız yeterlidir. Burada, döngüsel referanslar nedeniyle belleği temizlemek için gc.collect() işlevine ihtiyacımız var. Ancak, gerçek uygulamalarda gc.collect() işlevini manuel olarak çağırmanıza gerek yoktur çünkü herhangi bir işlem yapmamıza gerek kalmadan arka planda çalışır.

Sınıf methotları (@classmethod) veya static methotlar (@staticmethod) bu sorundan etkilenmez. Bu yöntemlerde, önbellek örnek için değil, sınıf için yereldir. Bu nedenle, @lru_cache decoratorü her zamanki gibi doğrudan kullanabilirsiniz.

@classmethod
@functools.lru_cache(maxsize=None)
def sum_of_squares(cls):
return sum([num**2 for num in range(cls.number+1)])

@staticmethod
@functools.lru_cache(maxsize=None)
def foo(a,b):
return a + b

✨ Built-in @cache decorator kullanarak önbelleğe alma

Python’da önbelleği kullanmanın başka bir yöntemi de @cache decoratorüdür. Memoize tekniğini kullanır ve @lru_cache(maxsize=None) ile aynı değeri döndürür. Bu decorator, eski değerleri ayıklaması gerekmediğinden, max_size sınırı olan bir @lru_cache decoratorden daha küçük ve daha hızlıdır.

// Syntax
@cache
def x():
pass
import time
from functools import cache

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

fib_result = fibonacci(25)
print(f"Result: {fib_result})

Fibonacci örneğimizi çalıştırdığımızda programın çalışması 0.029 milisaniyede tamamlandı. Bu sürenin önbelleksiz Fibonacci örneğinden daha hızlı olduğunu söyleyebiliriz.

✨Built-in @cached_property decorator kullanarak önbelleğe alma

@cached_property, uzun yıllardır Django Framework içinde yer alan ve Python’a Ekim 2019'da 3.8 sürümüyle eklenen bir dekoratördür.

@cached_property decorator, bir sınıf yöntemini yalnızca bir kez hesaplanan ve ardından bir örnek özniteliği olarak önbelleğe alınan bir özelliğe dönüştürür. Her erişimde işlevin değerini yeniden hesaplamaktan kaçınmanıza izin verdiği için, hesaplanması pahalı olan işlevler için kullanışlıdır.

Bir sınıf örneği oluştururken bir sayı alan Example sınıfını ve alınan sayıya kadar olan sayıların karelerinin toplamını bulan bir methot yazalım.

class Example:
def __init__(self, number):
self.number = number

def sum_of_squares(self):
return sum([num**2 for num in range(self.number+1)])

example = Example(1000)
example.sum_of_squares()
example.sum_of_squares()

print(f"Result: {example.sum_of_squares()}")

Sınıfın yöntemini 3 kez çağırdığımız bu örnekte kod 0.17 milisaniyede yürütüldü. Şimdi @cached_property decorator kullanarak programı tekrar yürütelim ve sonuca bakalım.

from functools import cached_property

class Example:
def __init__(self, number):
self.number = number

@cached_property
def sum_of_squares(self):
return sum([num**2 for num in range(self.number+1)])

example = Example(1000)
example.sum_of_squares
example.sum_of_squares

print(f"Result: {example.sum_of_squares}")

Program önbellek kullanmadan olduğundan daha hızlı şekilde 0,06 milisaniyede tamamlandı. Sınıf örneği silindiğinde önbellek temizlenecektir.

✏️ Önbelleğe alınan değer, örneğin bir özniteliği olarak saklanır, bu nedenle sınıfın her örneğine özel olacaktır. Bu, sınıfın birden çok örneğine sahipseniz, her örneğin işlev için kendi önbelleğe alınmış değerine sahip olacağı anlamına gelir.

Burada sum_of_sequnce işlevini çağırırken bir işlev gibi değil, öznitelik gibi çağrıldığını görüyorsunuz. @cached_property, önbelleğe alma eklenen @property decoratorüne benzer.

✏️ @cached_property ve @property decoratorleri farklı şekilde çalışır. @property yöntemi için bir setter metotu tanımlanmadıkça yalnızca salt okunur bir yöntem olarak çalışır. Bu nedenle, özniteliği yazma/değiştirme devre dışı bırakılır. @cached_property yöntemleri salt okunur değildir, bu nedenle özniteliği yazma/değiştirme devre dışı bırakılmaz.

@cached_property PEP-412, anahtar paylaşım sözlük işlemlerine müdahale eder. Bu nedenle sözlükler normalden daha fazla yer kaplar. Alan verimli anahtar paylaşımı kullanmak istiyorsanız veya değişken değişkenler/eşleme gerekli değilse, @cached_property decoratorü ile benzer çalışan bir yapı, @cache decoratorü üzerine eklenen @property decoratorü ile elde edilebilir:

from functools import cache

class Example:
def __init__(self, number):
self.number = number

@property
@cache
def sum_of_squares(self):
return sum([num**2 for num in range(self.number+1)])

example = Example(1000)
example.sum_of_squares
example.sum_of_squares

print(f"Result: {example.sum_of_squares}")

Önbelleğe alma, doğru şekilde ve doğru yerde kullanıldığında uygulamalarınızın hızını artıran önemli bir optimizasyon yöntemidir. Bu yazımızda Python’da yerleşik metotlarla önbelleğe almayı nasıl kullanabileceğimizi öğrendik. Umarım keyifli ve faydalı bir yazı olmuştur. Bir sonraki gönderide görüşmek üzere ^^

✨ Kaynaklar

[1] Functools in the official Python documentation
[2] Difference between functool’s cache and lru_cache
[3] don’t lru_cache methods!- Anthony explains(Youtube channel)
[4] Don’t wrap instance methods with ‘functools.lru_cache’ decorator in Python

--

--