Dekoratörler, Önbellek Kodlaması, Fonksiyon Değiştirmek
Bir fonksiyonu çağırıyoruz, ona bazı parametreler geçiyoruz, bu parametrelerle fonksiyon bir hesap yapıyor, bize sonucu veriyor. Mesela bir sayıya kadar olan tüm tam sayıları toplayan bir fonksiyon olsun,
def n_topla1(N):
tmp = list(range(N+1))
print (tmp)
res = np.sum(tmp)
return res
print (n_topla1(10))
print (n_topla1(5))
print (n_topla1(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
55
[0, 1, 2, 3, 4, 5]
15
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
55
Toplam 1 + 2 + 3 + .. + 10 diye 10'a kadar olan sayıları topladı ve döndürdü. Sonra 5 ve tekrar 10 için aynı teknik kullanıldı.
Fakat diyelim ki bu fonksiyonu pek çok kez ardı ardına çağırmak gerekiyor, ve çağrıların çoğu benzer parametreleri kullanacak, mesela 10'a kadar olan toplam pek çok kez yapılabilecek.. Acaba üstteki toplam işlemini bir kez yapıp ikinci, üçüncü çağrılarda aynı hesabı döndürsek olmaz mı?
Koda böyle bir ek yapılabilir. Mesela n_topla1
fonksiyonunda daha
başka bir şey yapmadan önce parametreleri biraraya koyarak bir tür
anahtar oluşturabiliriz, bu anahtarı bir sözlükte arama için
kullanırız, eğer değer bulunursa birisi önceden o hesabı yapıp oraya
koymuş demektir, fonksiyonda devam etmek yerine sözlükteki değeri
döndürürüz, hesaba gerek kalmaz. Tabii ki eğer sözlükte o değer yoksa,
hesabı yapıp sözlüğe bizim koymamız gerekir, böylece bir sonraki çağrı
yapan o değerleri bulsun.
"Parametrelerden anahtar oluşturmak", "varsa döndürmek yoksa oraya koymak" - burada bir sürü hamaliyesi fazla kodlama var. Bu kodları bir paket üzerinden, hatta bir fonksiyon başına koyulacak bir etiket / işaret / dekoratör üzerinden Python'a yaptırsak iyi olacak.
Hazır Paket Kullanarak
Paket cachetools
içinde böyle kodlar var [1]. Toplam foksiyonunu etiketleyelim,
from cachetools import cached
@cached(cache={})
def n_topla2(N):
tmp = list(range(N+1))
print (N, "=>", tmp)
res = np.sum(tmp)
return res
print (n_topla2(5))
print (n_topla2(10))
print (n_topla2(10))
5 => [0, 1, 2, 3, 4, 5]
15
10 => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
55
55
10 toplamı önbellekten geldi, onun listesi basılmadı dikkat edersek çünkü fonksiyonun o kısmına gidilmesi gerekmedi.
Önbellekleme etiketi @cached
ile kullanılacak sözlük tipini de
tanımlayabiliyoruz, üstteki örnekte standart bir Python sözlüğü {}
kullanıldı. Başka türlü sözlükler de var, bu sözlükler ayrıca
önbellekleme stratejisini değiştirmemize yarıyor. Mesela "sadece en
son 2 konulan öğe hatırlansın" istiyorsam, yani büyüklüğü 2'den fazla
olmasın, ve üçüncü öğeyi koymaya çalışırsam ilk eklediğim atılsın
istiyorsam, bu bir ilk giren ilk çıkar (first in first out -FIFO-)
mantığıdır, ve böyle bir sözlük tipi vardır, FIFOCache
.
from cachetools import FIFOCache
@cached(cache=FIFOCache(maxsize=2))
def n_topla2(N):
tmp = list(range(N+1))
print (N, "=>", tmp)
res = np.sum(tmp)
return res
print ('5 Toplami', n_topla2(5))
print ('10 Toplami',n_topla2(10))
print ('20 Toplami',n_topla2(20))
# tersten sor, son girenleri bulalim
print ('20 Toplami',n_topla2(20))
print ('10 Toplami',n_topla2(10))
print ('5 Toplami', n_topla2(5))
5 => [0, 1, 2, 3, 4, 5]
5 Toplami 15
10 => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
10 Toplami 55
20 => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
20 Toplami 210
20 Toplami 210
10 Toplami 55
5 => [0, 1, 2, 3, 4, 5]
5 Toplami 15
20,10 toplamları hatırlandı, ama 5 için tekrar hesap yapıldı çünkü büyüklüğü 2
olan FIFOCache
o sonuçları atmıştı.
Diğer bazı önbellek tipleri mesela LRUCache
en az kullanılan
objeleri atar. TTLCache
ise konulan her obje üzerinde bir zaman
aşımını kontrol eder, bunu ttl
parametresi ile saniye üzerinden
kullanıcı ayarlayabilir, mesela ttl=600
ile objeler konulduktan 10
dakika sonra onbellekten çıkartılırlar, "eskimiş" olurlar.
Diğer önbellek tipleri için [2].
Kendi Kodumuz İle
Önbellek kullanımını kendi kodumuz ile de ekleyebiliriz. Mesela kare almakla yükümlü bir fonksiyonumuz var diyelim (altta ise yaramaz bir değişken dummy ekledik, bazı püf noktaları gösterebilmek için),
def kare(dummy, a): return a*a
Bu fonksiyonu
kare("filan", 3)
diye çağırıyoruz ve sonuç olarak 9 gelmesini bekliyoruz.
Önbellekleme için, diyelim ki, eğer a değeri önceden görülmüşse, kare işlemi sonucunun tekrar hesaplanmasını istemiyoruz, onu onbellekten bulup hızlı bir şekilde geri döndürmek tercihimiz (tabii çarpım işlemi de çok hızlı işler, ama bu örnek için yavaş olabileceğini hayal edelim).
Bu kod uzerinde onbelleklemeyi eski usulle yapsaydik, kod suna benzerdi:
cache = {}
def kare(dummy, a):
if not a in cache: cache[a] = a*a
return cache[a]
Değişken cache bir Python sözlüğüdür ve onbelleğimiz onun üzerinde duruyor. Görüldüğü gibi kod biraz kalabalıklaştı. Onbellek objesi alanen ortada, ayrıca ıf gibi çok ciddi bir ibareyi koda sokuşturmak zorunda kaldık. Genellikle bu ifade önemli bir işlem mantığı var ise kullanılır - en azından kod okunabilirliği açısından böyle olması daha iyidir. Peki bu isi daha temiz bir sekilde yapamaz miydik?
Python dekoratör fonksiyonları işte tam burada ise yarar. Bir dekoratör bir fonsiyonu "sarmalayabilir (wrap)", ve o fonksiyona giren çıkan tüm değerler üzerinde işlem yapabilir, ve onları istediği gibi değiştirebilir, bu sayede o fonksiyona "çaktırmadan" ek özellikler verebilir. Sözdizim açısından da temiz dururlar, çünkü dekoratör fonksiyon üzerinde '@' ile tanımlanan bir şeydir, başka bir eke ihtiyaç yoktur. O zaman (önce dekoratörün kendisi)
def cache(function): memo = {}
def wrapper(*args):
if args[1] in memo:
print "cache hit"
return memo[args[1]]
else:
print "cache miss"
rv = function(*args)
memo[args[1]] = rv
return rv return wrapper
Üstteki kod ana kodunuzdan ayrı bir yerde, bir dosyada durabilir mesela, sadece bir kere yazılır zaten, ve kullanılması gerektiği zaman şu ibare yeterlidir,
@cache
def kare(dummy, a):
return a*a
Görüldüğü gibi gayet temiz. Onbellek kodu hiç etrafta gözükmüyor, bu da kod bakımını daha rahatlaştıran bir özellik. Böylece kare fonksiyonunu normalde olması gerektiği gibi yazıyoruz, kod onbellek olsa da olmasa da aynı şekilde yazılıyor, sadece çarpım için gereken işlem mantığını içeriyor.
Not: dummy değişkenini dekoratör içinde istediğimiz herhangi fonksiyon argümanı ile iş yapabileceğimizi göstermek için kullandık, args[1] ile sadece ikinci argümana baktık mesela.
Koda Ekler Enjekte Etmek
Diyelim ki mevcut bir kod parcasi var,
import randomclass Foo:
def f(self,x):
x = random.random()
return xo = Foo()
Biz bu kodun f()
çağrısını "yakalayıp" ona ek bir şeyler yaptırtmak
istiyoruz, ve mevcut koda hiç dokunmadan bunu yapmak istiyoruz. Belki
f() çağrısı bir başka yazılım paketi içinde, vs. Bu fonsiyonu dekore
ederek bunu yapabiliriz, fakat mevcut fonksiyon koduna dokunmak
istemediğimiz için metot üstünde @birdekoratör gibi bir kullanım
yapamayız. Bu durumda başka bir seçenek sudur,
def decorated_f(fn):
def new_f(*args, **kwargs):
res = fn(*args, **kwargs)
setattr(args[0], "was", res)
return res
return new_f
Foo.f = decorated_f(Foo.f)
Simdi
print "o", o.f(0)
print "was", o.was
Yeni ekleri de işletecek, bu ek Foo üzerinde yeni bir öğe yarattı, ve
bu öğeye o.was
diye erişiyoruz.
Kaynaklar
[1] https://pypi.org/project/cachetools/
[2] https://cachetools.readthedocs.io/en/latest/
Yukarı