Site Tools


python-lru-cache

Python LRU cache

Python LRU cache (from functools.lru_cache) memoizes function results in a fixed-size cache, keeping the most-recently-used items and evicting least-recently-used ones when full. Caching expensive computations (recursive functions, database queries, calculations) can dramatically improve performance—but only for functions with pure (no side effects) logic and hashable arguments.

Use LRU cache for expensive pure functions; be careful caching functions with side effects or mutable arguments.

Example

This example shows LRU caching speeding up expensive computations.

# run: python3 lru_cache.py
# description: function memoization with LRU cache
 
from functools import lru_cache
import time
 
# Expensive fibonacci without caching
def fib_slow(n):
    if n <= 1:
        return n
    return fib_slow(n - 1) + fib_slow(n - 2)
 
# Cached version
@lru_cache(maxsize=128)
def fib_fast(n):
    if n <= 1:
        return n
    return fib_fast(n - 1) + fib_fast(n - 2)
 
# Compare performance
print("Computing fib(30) without cache...")
start = time.time()
result = fib_slow(30)
elapsed = time.time() - start
print(f"Result: {result}, Time: {elapsed:.3f}s")
 
print("\nComputing fib(30) with cache...")
start = time.time()
result = fib_fast(30)
elapsed = time.time() - start
print(f"Result: {result}, Time: {elapsed:.6f}s")
 
# Cache info
print(f"\nCache info: {fib_fast.cache_info()}")
 
# Clearing cache
fib_fast.cache_clear()
print(f"After clear: {fib_fast.cache_info()}")
 
# Expensive computation (database lookup simulation)
@lru_cache(maxsize=32)
def lookup_user(user_id):
    """Simulated expensive database lookup."""
    print(f"  (looking up user {user_id}...)")
    time.sleep(0.1)
    return f"User {user_id}"
 
print("\n" + "="*60)
print("User lookups:")
print(lookup_user(1))
print(lookup_user(2))
print(lookup_user(1))  # cached, no lookup
print(lookup_user(2))  # cached, no lookup
 
print(f"\nCache hits: {lookup_user.cache_info().hits}")
print(f"Cache misses: {lookup_user.cache_info().misses}")
 
# With custom type hints (Python 3.9+)
@lru_cache(maxsize=64)
def expensive_computation(x: int, y: int) -> int:
    """Pure function suitable for caching."""
    return x ** y
 
print(f"\n{expensive_computation(2, 10)}")
print(f"Cache size: {expensive_computation.cache_info().currsize}")
 
# Disable cache (maxsize=None for unlimited, use carefully!)
@lru_cache(maxsize=None)
def unlimited_cache(n):
    """Unlimited cache (grows forever)."""
    return n * 2
 
# Disable caching
@lru_cache(maxsize=0)
def no_cache(n):
    """Cache disabled (no memoization)."""
    return n * 2

Common patterns

Basic usage:

  • @lru_cache(): decorate function to cache results
  • @lru_cache(maxsize=128): max cached entries (default 128)
  • Cached by argument values; arguments must be hashable

Cache info:

  • func.cache_info(): returns CacheInfo(hits, misses, maxsize, currsize)
  • Useful for monitoring cache effectiveness
  • func.cache_clear(): empty the cache

Hashable arguments only:

  • Works with ints, strings, tuples
  • Won't work with lists, dicts, sets (unhashable)
  • For mutable args, convert to immutable: tuple instead of list

Side effects and purity:

  • Caching only safe for pure functions (same output for same input)
  • Don't cache functions that write files, modify state, etc.
  • Don't cache functions with time-dependent results

Maxsize tuning:

  • Small maxsize: less memory, more cache misses
  • Large maxsize: more memory, fewer misses
  • maxsize=None: unlimited cache (grows forever; use carefully)
  • maxsize=0: caching disabled (decorator works but doesn't cache)

Performance trade-off:

  • Cache lookup has overhead; not always faster
  • Only beneficial for expensive functions
  • Worst case: cache lookup overhead with no hits
  • Test with cache_info to measure

Custom caching (when LRU insufficient):

  • functools.cache() (Python 3.9+): unlimited, simpler
  • functools.cached_property: cache class property
  • Roll custom cache for complex requirements

Thread-safety:

  • LRU cache is thread-safe (uses locks)
  • Slight performance cost for thread safety
  • Large multi-threaded workloads: consider alternatives

Typed version:

  • @lru_cache() caches f(1) and f(1.0) the same way
  • @functools.lru_cache(typed=True) treats as different (Python 3.8+)
  • Use when int and float distinctions matter

Common mistakes:

  • Caching pure functions that shouldn't be: if results are correct, it's fine
  • Not checking cache_info(); could be hurting performance
  • Caching huge objects; monitor memory usage
  • Arguments with large hash tables (custom hash expensive)
python-lru-cache.md · Last modified: by 127.0.0.1