# 76. Using functools.lru\_cache

***

**1. Basic Caching with `lru_cache`**

The `lru_cache` decorator caches the results of a function to optimize performance for expensive, repeated function calls.

```python
import functools

@functools.lru_cache(maxsize=3)
def expensive_function(x):
    print(f"Computing {x}...")
    return x * x

print(expensive_function(2))  # First call, computes the result
print(expensive_function(2))  # Second call, returns cached result
```

***

**2. Caching with `maxsize`**

The `maxsize` parameter determines how many results the cache can hold. If the cache exceeds the limit, the least recently used (LRU) result is removed.

```python
import functools

@functools.lru_cache(maxsize=2)
def square(x):
    print(f"Computing square of {x}...")
    return x * x

print(square(2))  # Computed
print(square(3))  # Computed
print(square(2))  # Cached
print(square(4))  # Computed, evicts the cache for 3
```

***

**3. Using `lru_cache` on Recursive Functions**

`lru_cache` is useful for caching results in recursive functions, such as computing Fibonacci numbers.

```python
import functools

@functools.lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))  # Fast calculation after caching previous results
```

***

**4. Checking Cache Statistics**

The `cache_info` method can be used to view statistics on the cache's performance, such as hits, misses, and the current cache size.

```python
import functools

@functools.lru_cache(maxsize=3)
def compute(n):
    return n * 2

compute(2)
compute(3)
compute(2)

print(compute.cache_info())  # Shows cache hits, misses, and current size
```

***

**5. Clearing the Cache with `cache_clear`**

You can clear the cache manually with the `cache_clear` method.

```python
import functools

@functools.lru_cache(maxsize=2)
def add(x, y):
    return x + y

add(2, 3)
add(4, 5)
print(add.cache_info())  # Shows cache hits/misses

add.cache_clear()  # Clears the cache
print(add.cache_info())  # Cache info after clearing
```

***

**6. Custom Cache Size**

You can adjust the `maxsize` parameter to store a specific number of results in the cache.

```python
import functools

@functools.lru_cache(maxsize=5)
def multiply(x, y):
    return x * y

multiply(2, 3)
multiply(4, 5)
multiply(6, 7)
multiply(8, 9)
multiply(10, 11)
multiply(12, 13)  # This call will evict the oldest cache entry

print(multiply.cache_info())
```

***

**7. Using `lru_cache` with Keyword Arguments**

`lru_cache` caches function results based on both positional and keyword arguments.

```python
import functools

@functools.lru_cache(maxsize=3)
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet("Alice"))  # First call, computed
print(greet("Alice"))  # Cached result
print(greet("Bob", greeting="Hi"))  # New result, not cached
```

***

**8. Caching Functions with No Arguments**

Even if a function doesn't take any arguments, you can still apply `lru_cache` to optimize repeated calls.

```python
import functools

@functools.lru_cache(maxsize=None)
def no_args():
    print("Executing no_args function...")
    return "Hello, world!"

print(no_args())  # First execution
print(no_args())  # Cached result
```

***

**9. Working with Immutable Arguments for Caching**

`lru_cache` requires that function arguments be hashable, as it uses these arguments to create a cache key.

```python
import functools

@functools.lru_cache(maxsize=2)
def process_data(data):
    print("Processing data...")
    return sum(data)

# List is mutable, so it cannot be used directly
# process_data([1, 2, 3])  # This would raise TypeError

# Use a tuple (immutable) instead
print(process_data((1, 2, 3)))  # Cached result
print(process_data((4, 5, 6)))  # Cached result
```

***

**10. Using `lru_cache` with Function Wrapping**

You can use `lru_cache` as part of a custom wrapper function to add caching behavior dynamically.

```python
import functools

def cache_decorator(func):
    cached_func = functools.lru_cache(maxsize=2)(func)
    return cached_func

@cache_decorator
def square(n):
    print(f"Squaring {n}")
    return n * n

print(square(2))  # First call
print(square(3))  # First call
print(square(2))  # Cached
print(square(4))  # New calculation, old cache entry evicted
```

***

These snippets demonstrate how `functools.lru_cache` can be used to improve performance by caching the results of functions, especially those that are called frequently with the same arguments. The decorator is versatile and works well with both simple and recursive functions, as well as handling keyword arguments.
