Speeding Up Your Python Code with the cache Decorator from functools

Sravanth
4 min readJan 29, 2024

In the vast world of Python programming, efficiency and speed often dictate the success of a project. One of the hidden gems in achieving this is the cache decorator from the functools module. This powerful tool can drastically reduce execution time by avoiding redundant computations, making it a must-know for Python enthusiasts and professionals alike.

Understanding the Cache Decorator

The cache decorator is a simple yet effective tool that stores the results of function calls, enabling subsequent calls with the same arguments to fetch results from the cache rather than executing the function again. This is particularly useful for functions that perform heavy or repetitive computations.

How It Works

  1. Initial Call: On the first call with a set of arguments, the function executes as usual, and the result is cached.
  2. Subsequent Calls: For any repeat calls with identical arguments, the result is instantly retrieved from the cache, bypassing the function execution.

With vs With Out cache — Example

alternative to cache. Let's use lru_cache for this example:

import time
from functools import lru_cache

# Fibonacci without cache
def fibonacci_no_cache(n):
if n < 2:
return n
return fibonacci_no_cache(n-1) + fibonacci_no_cache(n-2)

# Fibonacci with lru_cache (as a substitute for cache)
@lru_cache(maxsize=None) # Equivalent to cache in function
def fibonacci_with_cache(n):
if n < 2:
return n
return fibonacci_with_cache(n-1) + fibonacci_with_cache(n-2)

# Timing the Fibonacci function without cache
start_no_cache = time.time()
fibonacci_no_cache(30) # Using a smaller number to avoid long computation times
end_no_cache = time.time()
time_no_cache = end_no_cache - start_no_cache

# Timing the Fibonacci function with cache
start_with_cache = time.time()
fibonacci_with_cache(30)
end_with_cache = time.time()
time_with_cache = end_with_cache - start_with_cache

time_no_cache, time_with_cache
Result
(0.767594575881958, 9.34600830078125e-05)

This code defines two versions of the Fibonacci function: one without caching (fibonacci_no_cache) and one with caching using lru_cache (fibonacci_with_cache). It then measures the time it takes to compute the 30th Fibonacci number with each version. The version with lru_cache should be significantly faster due to the caching of intermediate results.

The execution times for computing the 30th Fibonacci number are as follows:

  • Without cache: approximately 0.7680 seconds
  • With cache (using lru_cache): approximately 0.0000930 seconds

Real-World Examples

To illustrate the power of the cache decorator, let's delve into some practical examples.

Example 1: Fibonacci Sequence

The Fibonacci sequence is a classic case where caching shines. Calculating higher numbers in the sequence without caching can be painfully slow due to the exponential increase in redundant calculations.

from functools import cache

@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10)) # 55
print(fibonacci(50)) # Significantly faster with caching

Example 2: Factorial Function

Similar to the Fibonacci sequence, calculating factorials involves repetitive computations, making it another perfect candidate for caching.

from functools import cache

@cache
def factorial(n):
if n == 0:
return 1
return n * factorial(n-1)
print(factorial(5)) # 120
print(factorial(10)) # 3628800

Example 3: Complex Computations

Caching isn’t limited to simple mathematical functions. It’s also beneficial for complex operations, such as simulating heavy computations.

from functools import cache
import time
@cache
def heavy_computation(x):
time.sleep(2) # Simulate a heavy computation
return x * x

start_time = time.time()
print(heavy_computation(10)) # Takes about 2 seconds
print("First call took", time.time() - start_time, "seconds")
start_time = time.time()
print(heavy_computation(10)) # Returns immediately
print("Second call took", time.time() - start_time, "seconds")

Advantages of Using cache

  • Performance Improvement: By avoiding redundant computations, the cache decorator significantly speeds up the execution of functions, especially those with expensive operations.
  • Simple Integration: Adding @cache above a function is all it takes to implement caching, making it an effortless way to enhance performance.
  • API Efficiency: When dealing with API calls, caching can reduce the number of requests, saving bandwidth and potentially avoiding rate limits. It’s especially useful for APIs that return static or infrequently changed data.

Caching API Calls Example

Suppose you have a function that fetches weather data for a specific location from a weather API. Weather data doesn’t change frequently (e.g., it might be sufficient to update every hour or even less frequently), so caching the results of this function can be very efficient.

Example: Caching Weather API Calls

from functools import cache
import requests
import time

# Mock API endpoint for demonstration
WEATHER_API_URL = "https://api.weatherapi.com/v1/current.json"
API_KEY = "your_api_key_here"
@cache
def get_weather(city):
params = {
"key": API_KEY,
"q": city
}
response = requests.get(WEATHER_API_URL, params=params)
data = response.json()
return data['current']['temp_c'], data['current']['condition']['text']
# Fetch weather data for the same city multiple times
city = "London"
start_time = time.time()
weather_info = get_weather(city)
print(f"Weather in {city}: {weather_info[0]}°C, {weather_info[1]}")
print("First call took", time.time() - start_time, "seconds")
# The second call should be faster due to caching
start_time = time.time()
weather_info = get_weather(city)
print(f"Weather in {city}: {weather_info[0]}°C, {weather_info[1]}")
print("Second call took", time.time() - start_time, "seconds")

In this example:

  • The get_weather function takes a city name as input and makes a GET request to a hypothetical weather API.
  • The first call to get_weather(city) will make an actual HTTP request to the API, and the response will be cached.
  • Subsequent calls to get_weather(city) with the same city name will return the cached result, avoiding additional HTTP requests and speeding up the response.

Conclusion

The cache decorator from Python's functools module is a potent tool in optimizing the performance of your Python code. Its simplicity and the immediate impact on efficiency make it an invaluable asset in any Python programmer's toolkit. Whether you're dealing with complex algorithms, heavy computations, or API calls, leveraging the cache decorator can lead to significant performance improvements with minimal effort.

--

--

No responses yet