36.1 @lru_cache and @cache: Memoization with a Bounded Cache
The functools.lfru_cache decorator (and its unbounded counterpart, @cache) provides a powerful mechanism for memoization—an optimization technique that stores the results of expensive function calls and returns the cached result when the same inputs occur again. This is particularly effective for functions that are deterministic and computationally intensive, such as those involving recursion, complex calculations, or I/O operations that can be buffered.
How LRU Cache Works Internally
The “LRU” in lru_cache stands for “Least Recently Used,” which describes its cache eviction policy. The decorator creates a dictionary that maps the function’s arguments to its return value. However, to prevent unbounded memory growth, the cache has a maximum size. When the cache is full and a new result needs to be stored, the least recently accessed entry (the one that hasn’t been used for the longest time) is discarded to make space. This is implemented efficiently using a doubly-linked list to track access order and a dictionary for fast lookups. The @cache decorator, introduced in Python 3.9, offers the same functionality but without a size limit, making it simpler to use when memory usage is not a concern, though this can be dangerous for functions with many possible inputs.
Syntax and Parameter Configuration
The decorator’s behavior is highly configurable through its parameters. The maxsize parameter defines the number of unique call results to cache. Setting it to None disables the LRU feature and allows the cache to grow without bound, similar to @cache, though @cache is more explicit. The typed parameter, when set to True, treats arguments of different types as distinct calls even if their values are equal. For example, f(3) and f(3.0) would be cached separately.
from functools import lru_cache
@lru_cache(maxsize=128, typed=False)
def factorial(n):
if n < 2:
return 1
return n * factorial(n - 1)
print(factorial(10)) # Calculation happens
print(factorial(10)) # Result is instantly returned from cache
print(factorial(10.0)) # Result returned from cache because typed=False
The Impact of Argument Mutability and Key Construction
A critical aspect of lru_cache is how it creates a key from the function arguments to use in its internal dictionary. All arguments must be hashable because the key must be immutable. If your function receives unhashable arguments, like a list or dict, the call will raise a TypeError. The decorator automatically handles positional and keyword arguments, but it’s crucial to understand that two calls are considered identical only if their arguments are equal and were passed in the same way.
@lru_cache
def cached_func(a, b):
return a + b
# These two calls are cached separately because the argument structure differs.
result1 = cached_func(1, 2) # Called as positional arguments
result2 = cached_func(a=1, b=2) # Called as keyword arguments
This behavior means that functions with default arguments can lead to multiple cache entries for the same logical call if the caller sometimes relies on defaults and sometimes provides the value explicitly.
Cache Inspection and Management
The decorated function is augmented with several methods to interact with the cache programmatically. The cache_info() method returns a named tuple (hits, misses, maxsize, currsize) providing statistics on the cache’s performance, which is invaluable for tuning the maxsize parameter. The cache_clear() method invalidates the entire cache, which is essential if the function’s output depends on external state that has changed or if you need to free memory.
def expensive_calculation(x):
time.sleep(1)
return x * x
cached_calc = lru_cache(maxsize=4)(expensive_calculation)
# First call is a miss, takes ~1 second
print(cached_calc(5))
print(cached_calc.cache_info()) # Shows hits=0, misses=1, currsize=1
# Second call with same arg is a hit, instantaneous
print(cached_calc(5))
print(cached_calc.cache_info()) # Shows hits=1, misses=1, currsize=1
# Clear the cache
cached_calc.cache_clear()
print(cached_calc.cache_info()) # Shows hits=0, misses=0, currsize=0
Common Pitfalls and Best Practices
- Stateless Functions: The most significant pitfall is using
lru_cacheon a function whose output depends on something other than its arguments (e.g., global variables, time, or external database state). The cache cannot know about these external changes, leading to stale, incorrect results.lru_cacheshould only be used on pure, deterministic functions. - Memory Management: With
@cacheormaxsize=None, the cache can grow indefinitely, potentially leading to memory exhaustion, especially for functions with a large parameter space. Always prefer a boundedmaxsizeunless you are certain of the input range. - Instance Methods: Caching an instance method can create unexpected memory leaks. The cache is stored on the function object, which is a property of the class. This means the cache persists for the lifetime of the interpreter and implicitly retains all
selfinstances used in calls, preventing garbage collection. To avoid this, carefully consider the scope or use aweakrefto the instance in a custom key. - Varying Return Types: Be cautious if a function can return different types of objects based on its input. The cache does not care about the return type; it only cares about the input key. This is usually not an issue but is important to remember for debugging.