Ana içeriğe geç

Python Önbelleği: İki Basit Yöntem

Python'da fonksiyonları önbelleğe almak için @functools.lru_cache veya @functools.cache gibi dekoratörleri kullanmayı öğrenin.
· 12 dk. oku

Yapay Zeka ile Keşfedin

Bu yazıda, Python'da önbelleğe almaya değineceğiz. Ne olduğunu ve nasıl etkili şekilde kullanılacağını anlayacağız.

Önbelleğe alma, uygulama performansını iyileştirmek için program tarafından elde edilen sonuçların geçici olarak saklanması ve daha sonra gerekirse yeniden kullanılması tekniğidir.

Bu eğitimde, @lru_cache ve @cache dekoratörleri de dahil olmak üzere Python'da önbelleğe almanın farklı tekniklerini öğreneceğiz; her ikisi de functools modülünde yer alır.

Acelesi olanlar için çok kısa bir önbellek uygulamasıyla başlayalım, ardından ayrıntılara devam edelim.

Kısa Cevap: Python Önbellek Uygulaması

Python'da bir önbellek oluşturmak için functools modülündeki @cache dekoratörünü kullanabiliriz. Aşağıdaki kodda, print() fonksiyonunun yalnızca bir kez çalıştığına dikkat edin:

import functools

@functools.cache
def square(n):
    print(f"Calculating square of {n}")
    return n * n

# Testing the cached function
print(square(4))
print(square(4))

# Calculating square of 4
# 16
# 16

Python'da Önbelleğe Alma Nedir?

Diyelim ki bir matematik problemini çözmemiz gerekiyor ve doğru cevabı bulmak bir saatimizi alıyor. Ertesi gün aynı problemi tekrar çözmek zorunda kalsaydık, en baştan başlamak yerine önceki çalışmamızı yeniden kullanmak faydalı olurdu.

Python'da önbelleğe alma benzer bir ilkeyi izler—fonksiyon çağrıları içinde hesaplandıklarında değerleri saklar ve bunlara tekrar ihtiyaç duyulduğunda yeniden kullanır. Bu tür önbelleğe alma, memoization olarak da adlandırılır.

Geniş bir sayı aralığının toplamını iki kez hesaplayan kısa bir örneğe bakalım:

output = sum(range(100_000_001))
print(output)
output = sum(range(100_000_001))
print(output)

# 5000000050000000
# 5000000050000000

Program her seferinde toplamı yeniden hesaplamak zorundadır. Bunu iki çağrıyı zamanlayarak doğrulayabiliriz:

import timeit

print(
    timeit.timeit(
        "sum(range(100_000_001))",
        globals=globals(),
        number=1,
    )
)

print(
    timeit.timeit(
        "sum(range(100_000_001))",
        globals=globals(),
        number=1,
    )
)

Çıktı, her iki çağrının da kabaca aynı süreyi aldığını gösterir (kurulumumuza bağlı olarak daha hızlı veya daha yavaş yürütme süreleri elde edebiliriz).

Bununla birlikte, aynı değeri birden fazla kez hesaplamaktan kaçınmak için bir önbellek kullanabiliriz. Yerleşik functools modülündeki cache() fonksiyonunu kullanarak sum adını yeniden tanımlayabiliriz:

import functools
import timeit

sum = functools.cache(sum)

print(
    timeit.timeit(
        "sum(range(100_000_001))",
        globals=globals(),
        number=1,
    )
)

print(
    timeit.timeit(
        "sum(range(100_000_001))",
        globals=globals(),
        number=1,
    )
)

İkinci çağrı artık bir saniyenin üzerinde zaman almak yerine birkaç mikrosaniye sürer; çünkü 0'dan 100.000.000'a kadar olan sayıların toplamını bulmanın sonucu zaten hesaplanmış ve önbelleğe alınmıştır—ikinci çağrı, daha önce hesaplanan ve saklanan değeri kullanır.

Yukarıda, functools.cache() dekoratörünü kullanarak yerleşik sum() fonksiyonuna bir önbellek ekliyoruz. Ayrıca bir not olarak, Python'da bir dekoratör, başka bir fonksiyonun davranışını, kodunu kalıcı olarak değiştirmeden değiştiren bir fonksiyondur. Dekoratörler hakkında daha fazla bilgiyi şu Python Dekoratörleri Eğitiminde edinebilirsiniz.

functools.cache() dekoratörü Python 3.9 sürümünde eklendi, ancak daha eski sürümler için functools.lru_cache() kullanabiliriz. Sonraki bölümde, @cache gibi daha sık kullanılan dekoratör gösterimini kullanmayı da içerecek şekilde, önbellek oluşturmanın her iki yolunu inceleyeceğiz.

Python Önbelleğe Alma: Farklı Yöntemler

Python'un functools modülü, fonksiyonlara önbellek uygulamak için iki dekoratör içerir. Bir örnekle functools.lru_cache() ve functools.cache()'i inceleyelim.

sum_digits() adında, bir sayı dizisini alan ve bu sayıların basamaklarının toplamını döndüren bir fonksiyon yazalım. Örneğin, girdi olarak (23, 43, 8) demeti kullanırsak:

  • 23'ün basamaklarının toplamı beştir.
  • 43'ün basamaklarının toplamı yedidir.
  • 8'in basamaklarının toplamı sekizdir.
  • Dolayısıyla genel toplam 20'dir.

sum_digits() fonksiyonumuzu şu şekilde yazabiliriz:

def sum_digits(numbers):
    return sum(
        int(digit) for number in numbers for digit in str(number)
    )

numbers = 23, 43, 8

print(sum_digits(numbers))

# 20

Farklı önbellek oluşturma yollarını keşfetmek için bu fonksiyonu kullanalım.

Python'da elle önbelleğe alma

Önce önbelleği elle oluşturalım. Bunu kolayca otomatikleştirebilsek de, önbelleği elle oluşturmak süreci anlamamıza yardımcı olur.

Bir sözlük oluşturalım ve fonksiyonu her yeni değerle çağırdığımızda sonuçları depolamak için anahtar-değer çiftleri ekleyelim. Eğer fonksiyonu bu sözlükte zaten saklı bir değerle çağırırsak, fonksiyon yeniden hesaplama yapmadan saklanan değeri döndürecektir:

import random
import timeit

def sum_digits(numbers):
    if numbers not in sum_digits.my_cache:
        sum_digits.my_cache[numbers] = sum(
            int(digit) for number in numbers for digit in str(number)
        )
    return sum_digits.my_cache[numbers]
sum_digits.my_cache = {}

numbers = tuple(random.randint(1, 1000) for _ in range(1_000_000))

print(
    timeit.timeit(
        "sum_digits(numbers)",
        globals=globals(),
        number=1
    )
)

print(
    timeit.timeit(
        "sum_digits(numbers)",
        globals=globals(),
        number=1
    )
)

sum_digits(numbers) fonksiyonuna yapılan ikinci çağrı, önbelleğe alınmış değeri kullandığı için ilkinden çok daha hızlıdır.

Yukarıdaki kodu şimdi daha ayrıntılı açıklayalım. Öncelikle, fonksiyon içinde kullanmamıza rağmen sum_digits.my_cache sözlüğünü fonksiyonu tanımladıktan sonra oluşturduğumuza dikkat edin.

sum_digits() fonksiyonu, fonksiyona geçirilen argümanın sum_digits.my_cache sözlüğündeki anahtarlardan biri olup olmadığını kontrol eder. Tüm basamakların toplamı yalnızca argüman önbellekte yoksa hesaplanır.

Fonksiyonu çağırırken kullandığımız argüman sözlükte anahtar olarak hizmet ettiğinden, karma (hashable) bir veri türü olmalıdır. Liste hashable değildir, bu nedenle sözlükte anahtar olarak kullanılamaz. Örneğin, numbers öğesini demet yerine bir listeyle değiştirmeyi deneyelim—bu bir TypeError yükseltecektir:

# ...

numbers = [random.randint(1, 1000) for _ in range(1_000_000)]

# ...
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'

Önbelleği elle oluşturmak öğrenme amaçları için harikadır, ancak şimdi bunu yapmanın daha hızlı yollarını keşfedelim.

functools.lru_cache() ile Python'da önbelleğe alma

Python, 3.2 sürümünden beri lru_cache() dekoratörüne sahiptir. Fonksiyon adının başındaki “lru”, "least recently used" (en son kullanılan) ifadesinin kısaltmasıdır. Önbelleği, sık kullanılan şeyleri saklayan bir kutu gibi düşünebiliriz—doldurduğunda, LRU stratejisi en uzun süredir kullanılmayan öğeyi atarak yenisine yer açar.

sum_digits() fonksiyonumuzu @functools.lru_cache ile süsleyelim:

import functools
import random
import timeit

@functools.lru_cache
def sum_digits(numbers):
    return sum(
        int(digit) for number in numbers for digit in str(number)
    )

numbers = tuple(random.randint(1, 1000) for _ in range(1_000_000))

print(
    timeit.timeit(
        "sum_digits(numbers)",
        globals=globals(),
        number=1
    )
)

print(
    timeit.timeit(
        "sum_digits(numbers)",
        globals=globals(),
        number=1
    )
)

Önbelleğe alma sayesinde, ikinci çağrı belirgin şekilde daha kısa sürede çalışır.

Varsayılan olarak, önbellek ilk 128 hesaplanan değeri saklar. Bu 128 yer dolduğunda, algoritma yeni değerlere yer açmak için en son kullanılan (LRU) değeri siler.

Fonksiyonu maxsize parametresiyle süsleyerek farklı bir maksimum önbellek boyutu belirleyebiliriz:

import functools
import random
import timeit

@functools.lru_cache(maxsize=5)
def sum_digits(numbers):
    return sum(
        int(digit) for number in numbers for digit in str(number)
    )

# ...

Bu durumda önbellek yalnızca beş değer saklar. Önbelleğin boyutunu sınırlamak istemiyorsak maxsize argümanını None olarak da ayarlayabiliriz.

functools.cache() ile Python'da önbelleğe alma

Python 3.9, daha basit ve daha hızlı bir önbellek dekoratörü içerir—functools.cache(). Bu dekoratörün iki temel özelliği vardır:

  • Maksimum boyutu yoktur—functools.lru_cache(maxsize=None) çağrısına benzer.
  • Tüm fonksiyon çağrılarını ve sonuçlarını saklar (LRU stratejisini kullanmaz). Bu, nispeten küçük çıktılara sahip fonksiyonlar için ya da önbellek boyutu sınırlamalarıyla ilgilenmemiz gerekmeyen durumlar için uygundur.

sum_digits() fonksiyonunda @functools.cache dekoratörünü kullanalım:

import functools
import random
import timeit

@functools.cache
def sum_digits(numbers):
    return sum(
        int(digit) for number in numbers for digit in str(number)
    )

numbers = tuple(random.randint(1, 1000) for _ in range(1_000_000))

print(
    timeit.timeit(
        "sum_digits(numbers)",
        globals=globals(),
        number=1
    )
)

print(
    timeit.timeit(
        "sum_digits(numbers)",
        globals=globals(),
        number=1
    )
)

sum_digits()'i @functools.cache ile süslemek, sum_digits'i functools.cache()'e atamaya denktir:

# ...

def sum_digits(numbers):
    return sum(
        int(digit) for number in numbers for digit in str(number)
    )

sum_digits = functools.cache(sum_digits)

Farklı bir içe aktarma stilini de kullanabileceğimizi unutmayın:

from functools import cache

Bu şekilde, fonksiyonlarımızı yalnızca @cache kullanarak süsleyebiliriz.

Diğer önbellekleme stratejileri

Python'un kendi araçları, en son kullanılan girdilerin yeni değerlere yer açmak için silindiği LRU önbellekleme stratejisini uygular.

Birkaç diğer önbellekleme stratejisine bakalım:

  • İlk giren, ilk çıkan (FIFO): Önbellek dolduğunda, yeni değerlere yer açmak için ilk eklenen öğe kaldırılır. LRU ile FIFO arasındaki fark, LRU'nun son zamanlarda kullanılan öğeleri önbellekte tutması, FIFO'nun ise kullanım durumuna bakılmaksızın en eski öğeyi atmasıdır.
  • Son giren, ilk çıkan (LIFO): Önbellek dolduğunda en son eklenen öğe kaldırılır. Bir kafeteryadaki tabak yığınını hayal edin. Yığına en son koyduğumuz tabak (son giren), ilk alacağımız tabaktır (ilk çıkan).
  • En son kullanılan (MRU): Önbellekte yer gerektiğinde, en son kullanılan değer atılır.
  • Rastgele değiştirme (RR): Bu strateji, yeni bir öğeye yer açmak için rastgele bir öğeyi atar.

Bu stratejiler, geçerli yaşam süresinin ölçümleriyle de birleştirilebilir—bu, önbellekteki bir verinin ne kadar süreyle geçerli veya ilgili kabul edildiğini ifade eder. Örneğin, önbellekte bir haber makalesini düşünün. Sıkça erişiliyor olabilir (LRU onu tutar), ancak bir haftanın ardından haber güncelliğini yitirebilir.

Python Önbelleğe Alma: Yaygın Kullanım Alanları

Şimdiye kadar, öğrenme amaçlı basit örnekler kullandık. Ancak önbelleğe almanın gerçek dünyada pek çok uygulaması vardır.

Veri biliminde, sıklıkla büyük veri kümeleri üzerinde tekrarlanan işlemler yürütürüz. Önbelleğe alınmış sonuçları kullanmak, aynı veri kümeleri üzerinde aynı hesaplamaları tekrar tekrar yapmanın zaman ve maliyetini azaltır.

Ayrıca dış kaynakları kaydetmek için de önbelleğe almayı kullanabiliriz; örneğin web sayfaları veya veritabanları. Bir örnek düşünelim ve bir DataCamp makalesini önbelleğe alalım. Ancak önce terminalde aşağıdaki satırı çalıştırarak üçüncü taraf requests modülünü kurmamız gerekecek:

$ python -m pip install requests

requests kurulduktan sonra, aynı DataCamp makalesini iki kez getirmeye çalışan ve @lru_cache dekoratörünü kullanan aşağıdaki kodu deneyebiliriz:

import requests
from functools import lru_cache

@lru_cache(maxsize=10)
def get_article(url):
    print(f"Fetching article from {url}")
    response = requests.get(url)
    return response.text

print(get_article("https://www.datacamp.com/tutorial/decorators-python"))
print(get_article("https://www.datacamp.com/tutorial/decorators-python"))

Bir not olarak, çıktı çok uzun olduğundan kısalttık. Ancak, get_article() 'a yapılan yalnızca ilk çağrının Fetching article from {url} ifadesini yazdırdığına dikkat edin.

Bunun nedeni, web sayfasına yalnızca ilk çağrıda erişilmesidir. Sonuç, fonksiyonun önbelleğinde saklanır. Aynı web sayfasını ikinci kez istediğimizde, önbellekte saklanan veriler döndürülür.

Önbelleğe alma, aynı verileri tekrar tekrar getirirken gereksiz gecikmelerin yaşanmasını önler. Harici API'ler genellikle hız sınırlarına ve veri getirme maliyetlerine sahiptir. Önbelleğe alma, API maliyetlerini azaltır ve hız sınırlarına takılma olasılığını düşürür.

Bir diğer yaygın kullanım alanı da, bir dizi maliyetli hesaplamanın tekrarlanması gereken makine öğrenmesi uygulamalarıdır. Örneğin, bir metni bir makine öğrenmesi modelinde kullanmadan önce tokenleştirip vektörleştirmemiz gerekiyorsa, çıktıyı bir önbellekte saklayabiliriz. Böylece hesaplama açısından pahalı işlemleri tekrar etmemize gerek kalmaz.

Python'da Önbelleğe Alma Sırasında Yaygın Zorluklar

Python'da önbelleğe almanın avantajlarını öğrendik. Bir önbellek uygularken akılda tutulması gereken bazı zorluklar ve sakıncalar da vardır:

  • Önbellek geçersiz kılma ve tutarlılık: Veriler zamanla değişebilir. Bu nedenle, önbellekte saklanan değerlerin de güncellenmesi veya kaldırılması gerekebilir.
  • Bellek yönetimi: Büyük miktarda veriyi önbellekte saklamak bellek gerektirir ve önbellek sınırsız büyürse performans sorunlarına yol açabilir.
  • Karmaşıklık: Önbellek eklemek, önbelleği oluştururken ve sürdürürken sisteme karmaşıklık katar. Çoğu zaman faydalar bu maliyetleri aşsa da, artan bu karmaşıklık bulunması ve düzeltilmesi zor hatalara yol açabilir.

Sonuç

Aynı veriler üzerinde hesaplama açısından yoğun işlemler tekrarlanıyorsa, performansı iyileştirmek için önbelleğe alma kullanabiliriz.

Python'da, fonksiyon çağrılarında önbellek oluşturmak için functools modülünde yer alan @lru_cache ve @cache olmak üzere iki dekoratör bulunur.

Yine de, önbelleği güncel tuttuğumuzdan ve belleği düzgün yönettiğimizden emin olmamız gerekir.

Önbelleğe alma ve Python hakkında daha fazla öğrenmek isterseniz, altı dersten oluşan şu Python Programming yetkinlik yoluna göz atın.

Stephen Gruppetta

Yazar

Stephen Gruppetta

Malta Üniversitesi’nde lisans düzeyinde Fizik ve Matematik okudum. Ardından Londra’ya taşındım ve Imperial College’da Fizik alanında doktoramı tamamladım. İnsan retinasını görüntülemek için yeni optik teknikler üzerine çalıştım. Şimdi Python hakkında yazmaya, Python’u anlatmaya ve Python öğretmeye odaklanıyorum.

Konular

İlgili