Revisit the important concept of decorator in Python.
Decorator is so beautiful and functional that it saves time and makes the codes succinct. The timer decorator is a classical example; skip detailed explanation of timer decorator here since it’s cliché.
I would like to dive into Python’s functools module, which comes with
lru_cachepartialreducecmp_to_keytotal_orderingwrapssingledispatchcached_propertyupdate_wrapperidentitycache
It’s worth detailing use of cache and wraps!
First, wraps preserve the meta data of the original funciton
from functools import wraps
def my_decorator(func):
@wraps(func) # This preserves the original function's metadata
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@my_decorator
def add(a, b):
"""Adds two numbers."""
return a + b
# Using the decorated function
result = add(3, 4)
print(f"Result: {result}")
# Checking the function's metadata
print(f"Function name: {add.__name__}")
print(f"Function docstring: {add.__doc__}")
Cache to avoid repeated calculations. here is a code illustration
from functools import cache
import time
@cache
def slow_fib(n):
"""Compute the nth Fibonacci number (inefficiently)."""
time.sleep(1) # Simulate a slow calculation
if n < 2:
return n
return slow_fib(n - 1) + slow_fib(n - 2)
# Test the caching behavior
start_time = time.time()
print(slow_fib(10)) # First call, should take time
print(f"First call took: {time.time() - start_time:.2f} seconds")
start_time = time.time()
print(slow_fib(10)) # Second call, should be instant due to caching
print(f"Second call took: {time.time() - start_time:.2f} seconds")
start_time = time.time()
print(slow_fib(9)) # This call will be slow because it's not cached yet
print(f"Third call took: {time.time() - start_time:.2f} seconds")
start_time = time.time()
print(slow_fib(9)) # This call will be instant due to caching
print(f"Fourth call took: {time.time() - start_time:.2f} seconds")
55
First call took: 10.00 seconds
55
Second call took: 0.00 seconds
34
Third call took: 1.00 seconds
34
Fourth call took: 0.00 seconds
How does cache decorator built-in in Python accomplish this? it’s due to Memoization
Retrieval of Cached Results: On subsequent calls with the same arguments, the decorator first checks if the result is already in the cache. If it is, it returns the cached value instead of recalculating.
Storage of Results: When a function decorated with @cache is called, it stores the result in a dictionary (or a similar structure) using the function’s arguments as keys.
In actual practice, caching can improve the performance of web applications by reducing the need for repeated expensive computations or database queries. This pattern is commonly found in many codebases that require optimization for speed and efficiency.
from flask import Flask, jsonify
from functools import cache
import time
app = Flask(__name__)
# Simulate a slow database query or computation
@cache
def get_expensive_data(query):
time.sleep(2) # Simulate a delay
# In a real scenario, you'd fetch data from a database or perform a complex calculation
return {"data": f"Result for {query}"}
@app.route('/data/<query>')
def fetch_data(query):
result = get_expensive_data(query)
return jsonify(result)
if __name__ == '__main__':
app.run(debug=True)