-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
180: The functools Module: lru_cache for Memoization
There's a specific kind of frustration that happens when you write a piece of code that is logically perfect, but practically useless because it's too slow. I've been there more times than I'd like to admit. Usually, this happens with recursive functions where the program ends up calculating the exact same value thousands of times over.
Today we're looking at lru_cache from the functools module. "LRU" stands for Least Recently Used. In plain English, it's a decorator that remembers the results of a function for specific arguments. If you call the function again with those same arguments, Python doesn't re-run the logic; it just hands you the answer from memory. This is called memoization.
The Naive Approach to Recursion
Let's look at a classic example: calculating a Fibonacci number. I'll start with the most straightforward recursive implementation. It's clean, it's readable, and it's a performance nightmare.
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # Fast enough
print(fibonacci(35)) # Noticeable lag
print(fibonacci(40)) # Now we're really waiting...
The problem here is the redundancy. To calculate fibonacci(40), the computer has to calculate fibonacci(39) and fibonacci(38). But to calculate fibonacci(39), it also has to calculate fibonacci(38). We are recalculating the same branches of the tree over and over again. It's an exponential explosion of work.
Letting functools Handle the Heavy Lifting
I could write my own dictionary to store these results, but that's boilerplate I don't want to maintain. Instead, I'll use lru_cache. It wraps the function and handles the storage logic for me behind the scenes.
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # Instantaneous
By adding that one line, we turned an exponential time complexity into linear time. I used maxsize=None here, which tells Python to grow the cache as needed. In a real production environment, you might set this to something like 128 to prevent the cache from eating up all your RAM if the input space is massive.
The Hashability Trap
Now, here is where I usually trip up when I'm rushing. I decided to expand this into a function that calculates a "score" based on a list of multipliers. I thought I'd just cache the result to speed up repeated calculations with the same list.
@lru_cache(maxsize=None)
def calculate_score(multipliers):
print("Computing...")
return sum(multipliers) * 10
# This looks fine in my head
my_list = [1, 2, 3]
print(calculate_score(my_list))
If you run this, Python will throw a TypeError: unhashable type: 'list'. I forgot a fundamental rule of caching: lru_cache uses a dictionary internally to map arguments to results. Dictionary keys must be hashable. Since lists are mutable (they can change), they aren't hashable.
The fix is simple: use a tuple. Tuples are immutable and therefore hashable. If you're receiving a list from another part of your app, just cast it to a tuple before passing it into the cached function.
# The corrected version
my_list = [1, 2, 3]
print(calculate_score(tuple(my_list))) # Works perfectly
It's a small detail, but it's the difference between a crashing app and a fast one. Just remember: if you're caching a function, the arguments you pass in must be immutable.
📋 Practical Task
Exercise: Optimizing a Recursive Nested Category Total
You are building a system for an e-commerce store that calculates the total value of a product category. Some categories are nested inside others, and the calculation is recursive. Because the category tree is deep and often accessed repeatedly, the current implementation is too slow.
Your Task:
Implement lru_cache to optimize the get_category_value function. Ensure that the function can handle repeated calls for the same category ID efficiently. Then, test it by calling the function for the same category twice and verifying that the "Calculating..." print statement only executes once.
from functools import lru_cache
# Simulated database of category values and their sub-category IDs
CATEGORY_DATA = {
"Electronics": {"value": 100, "subs": ["Laptops", "Phones"]},
"Laptops": {"value": 50, "subs": ["Gaming", "Ultrabooks"]},
"Phones": {"value": 30, "subs": []},
"Gaming": {"value": 20, "subs": []},
"Ultrabooks": {"value": 15, "subs": []},
}
# TODO: Apply lru_cache here
def get_category_value(cat_id):
print(f"Calculating value for {cat_id}...")
data = CATEGORY_DATA.get(cat_id, {"value": 0, "subs": []})
total = data["value"]
for sub_id in data["subs"]:
total += get_category_value(sub_id)
return total
# Test your implementation
print(f"First call: {get_category_value('Electronics')}")
print("-" * 20)
print(f"Second call: {get_category_value('Electronics')}")
There are no comments for now.