Kursus
Dalam artikel ini, kita akan mempelajari caching di Python. Kita akan memahami apa itu dan bagaimana menggunakannya secara efektif.
Caching adalah teknik untuk meningkatkan performa aplikasi dengan menyimpan sementara hasil yang diperoleh program agar dapat digunakan kembali jika diperlukan nanti.
Dalam tutorial ini, kita akan mempelajari berbagai teknik caching di Python, termasuk decorator @lru_cache dan @cache dalam modul functools.
Bagi Anda yang ingin cepat, mari mulai dengan implementasi caching yang sangat singkat lalu kita lanjutkan dengan detailnya.
Jawaban Singkat: Implementasi Caching di Python
Untuk membuat cache di Python, kita dapat menggunakan decorator @cache dari modul functools. Pada kode di bawah ini, perhatikan bahwa fungsi print() hanya dijalankan sekali:
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
Apa itu Caching di Python?
Misalkan kita perlu menyelesaikan sebuah masalah matematika dan menghabiskan satu jam untuk mendapatkan jawaban yang tepat. Jika kita harus menyelesaikan masalah yang sama keesokan harinya, akan sangat membantu jika kita dapat menggunakan kembali pekerjaan sebelumnya alih-alih memulai dari awal.
Caching di Python mengikuti prinsip serupa—ia menyimpan nilai saat nilai tersebut dihitung di dalam pemanggilan fungsi untuk digunakan kembali ketika diperlukan lagi. Jenis caching ini juga disebut sebagai memoization.
Mari kita lihat contoh singkat yang menghitung jumlah rentang angka besar dua kali:
output = sum(range(100_000_001))
print(output)
output = sum(range(100_000_001))
print(output)
# 5000000050000000
# 5000000050000000
Program harus menghitung jumlahnya setiap kali. Kita dapat mengonfirmasinya dengan mengukur waktu pada kedua pemanggilan:
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,
)
)
Output menunjukkan bahwa kedua pemanggilan memakan waktu kurang lebih sama (bergantung pada setelan kita, waktu eksekusi bisa lebih cepat atau lebih lambat).
Namun, kita dapat menggunakan cache untuk menghindari perhitungan nilai yang sama lebih dari sekali. Kita dapat mendefinisikan ulang nama sum menggunakan fungsi cache() dalam modul bawaan functools:
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,
)
)
Pemanggilan kedua kini hanya memakan beberapa mikrodetik alih-alih lebih dari satu detik karena hasil penjumlahan angka dari 0 hingga 100.000.000 sudah dihitung dan disimpan dalam cache—pemanggilan kedua menggunakan nilai yang telah dihitung dan disimpan sebelumnya.
Di atas, kita menggunakan decorator functools.cache() untuk menambahkan cache pada fungsi bawaan sum(). Sebagai catatan, decorator di Python adalah fungsi yang mengubah perilaku fungsi lain tanpa mengubah kodenya secara permanen. Anda dapat mempelajari lebih lanjut tentang decorator di Tutorial Python Decorators ini.
Decorator functools.cache() ditambahkan ke Python pada versi 3.9, tetapi kita dapat menggunakan functools.lru_cache() untuk versi yang lebih lama. Pada bagian berikutnya, kita akan mengeksplorasi kedua cara ini untuk membuat cache, termasuk menggunakan notasi decorator yang lebih sering digunakan, seperti @cache.
Caching di Python: Berbagai Metode
Modul functools di Python memiliki dua decorator untuk menerapkan caching pada fungsi. Mari kita jelajahi functools.lru_cache() dan functools.cache() dengan sebuah contoh.
Mari kita tulis fungsi sum_digits() yang menerima sebuah deret angka dan mengembalikan jumlah digit dari angka-angka tersebut. Misalnya, jika kita menggunakan tuple (23, 43, 8) sebagai masukan, maka:
- Jumlah digit dari
23adalah lima. - Jumlah digit dari
43adalah tujuh. - Jumlah digit dari
8adalah delapan. - Jadi, jumlah keseluruhannya adalah 20.
Berikut salah satu cara kita dapat menulis fungsi sum_digits():
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
Mari gunakan fungsi ini untuk mengeksplorasi berbagai cara membuat cache.
Caching manual di Python
Mari kita buat cache secara manual terlebih dahulu. Meskipun ini juga bisa dengan mudah diotomatisasi, membuat cache secara manual membantu kita memahami prosesnya.
Mari buat sebuah dictionary dan tambahkan pasangan key-value setiap kali kita memanggil fungsi dengan nilai baru untuk menyimpan hasilnya. Jika kita memanggil fungsi dengan nilai yang sudah tersimpan dalam dictionary ini, fungsi akan mengembalikan nilai yang tersimpan tanpa menghitung ulang:
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
)
)
Pemanggilan kedua ke sum_digits(numbers) jauh lebih cepat daripada yang pertama karena menggunakan nilai yang disimpan dalam cache.
Sekarang mari kita jelaskan kode di atas lebih detail. Pertama, perhatikan bahwa kita membuat dictionary sum_digits.my_cache setelah mendefinisikan fungsi meskipun kita menggunakannya dalam definisi fungsi.
Fungsi sum_digits() memeriksa apakah argumen yang diteruskan ke fungsi sudah menjadi salah satu key dalam dictionary sum_digits.my_cache. Penjumlahan semua digit hanya dievaluasi jika argumen belum ada di cache.
Karena argumen yang kita gunakan saat memanggil fungsi berperan sebagai key dalam dictionary, argumen tersebut harus bertipe data yang hashable. List tidak hashable, sehingga tidak bisa digunakan sebagai key dalam dictionary. Misalnya, mari coba mengganti numbers dengan list alih-alih tuple—ini akan memunculkan TypeError:
# ...
numbers = [random.randint(1, 1000) for _ in range(1_000_000)]
# ...
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'
Membuat cache secara manual bagus untuk tujuan pembelajaran, tetapi sekarang mari kita jelajahi cara yang lebih cepat untuk melakukannya.
Caching Python dengan functools.lru_cache()
Python telah memiliki decorator lru_cache() sejak versi 3.2. “lru” di awal nama fungsi berarti "least recently used" (paling jarang digunakan terakhir). Kita bisa mengibaratkan cache sebagai kotak untuk menyimpan barang yang sering digunakan—saat penuh, strategi LRU membuang item yang paling lama tidak kita gunakan untuk memberi ruang bagi yang baru.
Mari hiasi fungsi sum_digits() kita dengan @functools.lru_cache:
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
)
)
Berkat caching, pemanggilan kedua memakan waktu jauh lebih singkat.
Secara default, cache menyimpan 128 nilai pertama yang dihitung. Setelah semua 128 tempat terisi, algoritma menghapus nilai yang paling jarang digunakan (LRU) untuk memberi ruang bagi nilai baru.
Kita dapat menetapkan ukuran maksimum cache yang berbeda saat menghias fungsi menggunakan parameter maxsize:
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)
)
# ...
Dalam kasus ini, cache hanya menyimpan lima nilai. Kita juga dapat mengatur argumen maxsize ke None jika kita tidak ingin membatasi ukuran cache.
Caching Python dengan functools.cache()
Python 3.9 menyertakan decorator caching yang lebih sederhana dan lebih cepat—functools.cache(). Decorator ini memiliki dua karakteristik utama:
- Ia tidak memiliki ukuran maksimum—mirip dengan memanggil
functools.lru_cache(maxsize=None). - Ia menyimpan semua pemanggilan fungsi dan hasilnya (tidak menggunakan strategi LRU). Ini cocok untuk fungsi dengan output yang relatif kecil atau ketika kita tidak perlu khawatir tentang batasan ukuran cache.
Mari gunakan decorator @functools.cache pada fungsi sum_digits():
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
)
)
Menghias sum_digits() dengan @functools.cache setara dengan menugaskan sum_digits ke functools.cache():
# ...
def sum_digits(numbers):
return sum(
int(digit) for number in numbers for digit in str(number)
)
sum_digits = functools.cache(sum_digits)
Perhatikan bahwa kita juga dapat menggunakan gaya impor yang berbeda:
from functools import cache
Dengan cara ini, kita dapat menghias fungsi hanya dengan @cache.
Strategi caching lainnya
Alat bawaan Python menerapkan strategi caching LRU, di mana entri yang paling jarang digunakan akan dihapus untuk memberi ruang bagi nilai baru.
Mari kita lihat beberapa strategi caching lain:
- First-in, first-out (FIFO): Saat cache penuh, item pertama yang ditambahkan akan dihapus untuk memberi ruang bagi nilai baru. Perbedaan antara LRU dan FIFO adalah LRU mempertahankan item yang baru-baru ini digunakan di cache, sedangkan FIFO membuang item tertua tanpa memedulikan penggunaannya.
- Last-in, first-out (LIFO): Item yang paling baru ditambahkan akan dihapus saat cache penuh. Bayangkan setumpuk piring di kafetaria. Piring yang paling terakhir kita letakkan (last in) adalah yang pertama kita ambil (first out).
- Most-recently used (MRU): Nilai yang paling baru digunakan akan dibuang ketika diperlukan ruang dalam cache.
- Random replacement (RR): Strategi ini secara acak membuang sebuah item untuk memberi ruang bagi item baru.
Strategi-strategi ini juga dapat dikombinasikan dengan ukuran masa berlaku—ini mengacu pada berapa lama suatu data dalam cache dianggap valid atau relevan. Bayangkan sebuah artikel berita dalam cache. Mungkin sering diakses (LRU akan mempertahankannya), tetapi setelah seminggu, beritanya bisa jadi sudah usang.
Caching di Python: Kasus Penggunaan Umum
Sejauh ini, kita menggunakan contoh yang sederhana untuk tujuan pembelajaran. Namun, caching memiliki banyak aplikasi di dunia nyata.
Dalam data science, kita sering mengeksekusi operasi berulang pada dataset besar. Menggunakan hasil yang disimpan dalam cache mengurangi waktu dan biaya yang terkait dengan melakukan perhitungan yang sama berulang kali pada dataset yang sama.
Kita juga dapat menggunakan caching untuk menyimpan sumber daya eksternal seperti halaman web atau basis data. Mari pertimbangkan sebuah contoh dan menyimpan artikel DataCamp ke dalam cache. Namun pertama-tama kita perlu memasang modul pihak ketiga requests dengan menjalankan baris berikut di terminal:
$ python -m pip install requests
Setelah requests terpasang, kita dapat mencoba kode berikut, yang berupaya mengambil artikel DataCamp yang sama dua kali sambil menggunakan decorator @lru_cache:
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"))
Sebagai catatan, kami memangkas output karena sangat panjang. Perhatikan, bagaimanapun, bahwa hanya pemanggilan pertama ke get_article() yang mencetak frasa Fetching article from {url}.
Ini karena laman web hanya diakses pada pemanggilan pertama. Hasilnya disimpan dalam cache fungsi. Ketika kita meminta laman web yang sama untuk kedua kalinya, data yang disimpan di cache yang dikembalikan.
Caching memastikan tidak ada penundaan yang tidak perlu saat mengambil data yang sama berulang kali. API eksternal juga sering memiliki batas laju dan biaya yang terkait dengan pengambilan data. Caching mengurangi biaya API dan kemungkinan terkena batas laju.
Kasus penggunaan umum lainnya ada pada aplikasi machine learning di mana beberapa komputasi mahal perlu diulang. Misalnya, jika kita perlu melakukan tokenisasi dan vektorisasi teks sebelum menggunakannya dalam model machine learning, kita dapat menyimpan outputnya di cache. Dengan cara ini kita tidak perlu mengulang operasi yang mahal secara komputasi.
Tantangan Umum Saat Melakukan Caching di Python
Kita telah mempelajari keuntungan caching di Python. Ada juga beberapa tantangan dan kekurangan yang perlu diingat saat mengimplementasikan cache:
- Invalidasi dan konsistensi cache: Data dapat berubah seiring waktu. Oleh karena itu, nilai yang disimpan dalam cache mungkin juga perlu diperbarui atau dihapus.
- Manajemen memori: Menyimpan data dalam jumlah besar di cache memerlukan memori, dan hal ini dapat menyebabkan masalah performa jika cache tumbuh tanpa batas.
- Kompleksitas: Menambahkan cache memperkenalkan kompleksitas pada sistem saat membuat dan memelihara cache. Sering kali, manfaatnya lebih besar daripada biayanya, tetapi peningkatan kompleksitas ini dapat menyebabkan bug yang sulit ditemukan dan diperbaiki.
Kesimpulan
Kita dapat menggunakan caching untuk mengoptimalkan performa ketika operasi yang intensif komputasi diulang pada data yang sama.
Python memiliki dua decorator untuk membuat cache saat memanggil fungsi: @lru_cache dan @cache dalam modul functools.
Namun, kita perlu memastikan cache tetap mutakhir dan memori dikelola dengan baik.
Jika Anda ingin mempelajari lebih lanjut tentang caching dan Python, lihat jalur keterampilan enam kursus Python Programming ini.
Saya belajar Fisika dan Matematika pada tingkat sarjana di University of Malta. Kemudian, saya pindah ke London dan meraih gelar PhD dalam bidang Fisika dari Imperial College. Saya meneliti teknik optik baru untuk memindai retina manusia. Sekarang, saya berfokus menulis tentang Python, berkomunikasi tentang Python, dan mengajar Python.

