Teaching Kids Programming – The @cache Function Decorator in Python


Teaching Kids Programming: Videos on Data Structures and Algorithms

Introduction to the Function Decorator in Python

In Python, we can add decorate a function by using the @ symbol, following by the decorator. For example:

1
2
3
4
5
6
7
8
9
10
def sample_decorator(func):
    def wrapper(n):
        print("Before")
        ans = func(n)
        print("After")
    return wrapper
 
@sample_decorator
def say_hello(s):
    print(f"Hello {s}")
def sample_decorator(func):
    def wrapper(n):
        print("Before")
        ans = func(n)
        print("After")
    return wrapper

@sample_decorator
def say_hello(s):
    print(f"Hello {s}")

Then we call say_hello(“World”), then we can see the following output:

1
2
3
Before
Hello World
After
Before
Hello World
After

The function decorator takes a single parameter which is the function, and the function decorator returns a wrapper function. We can use the (*args, **kwargs) which is a Python syntax used to define functions that can accept any number of positional and keyword arguments.

*args: This syntax collects any number of positional arguments into a tuple named args. When a function is called with positional arguments, they are collected into a tuple. Inside the function, you can access these arguments using the args tuple.

**kwargs: This syntax collects any number of keyword arguments into a dictionary named kwargs. When a function is called with keyword arguments, they are collected into a dictionary. Inside the function, you can access these arguments using the kwargs dictionary.

Here’s an example to illustrate how *args and **kwargs work:

1
2
3
4
5
6
def example_func(*args, **kwargs):
    print("Positional arguments (args):", args)
    print("Keyword arguments (kwargs):", kwargs)
 
# Calling the function with different arguments
example_func(1, 2, 3, a='apple', b='banana', c='cherry')
def example_func(*args, **kwargs):
    print("Positional arguments (args):", args)
    print("Keyword arguments (kwargs):", kwargs)

# Calling the function with different arguments
example_func(1, 2, 3, a='apple', b='banana', c='cherry')

Output:

1
2
Positional arguments (args): (1, 2, 3)
Keyword arguments (kwargs): {'a': 'apple', 'b': 'banana', 'c': 'cherry'}
Positional arguments (args): (1, 2, 3)
Keyword arguments (kwargs): {'a': 'apple', 'b': 'banana', 'c': 'cherry'}

In this example, args contains the positional arguments (1, 2, 3), and kwargs contains the keyword arguments {‘a’: ‘apple’, ‘b’: ‘banana’, ‘c’: ‘cherry’}. This syntax allows you to create flexible functions that can handle various types and numbers of arguments.

The @cache Function Decorator

The @cache decorator can be implemented using Python’s built-in functools module, specifically the lru_cache decorator. This decorator caches the results of a function call and returns the cached result when the same inputs occur again.

Here’s an implementation of the @cache decorator using functools.lru_cache to solve the Fibonacci Numbers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from functools import lru_cache
 
def cache(func):
    # Using functools.lru_cache to cache function results
    cached_func = lru_cache()(func)
 
    def wrapper(*args, **kwargs):
        return cached_func(*args, **kwargs)
 
    # Copying attributes of the original function
    wrapper.__name__ = func.__name__
    wrapper.__doc__ = func.__doc__
    wrapper.__module__ = func.__module__
 
    return wrapper
 
# Example usage:
@cache
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)
 
# Test the fibonacci function with caching
print(fibonacci(10))  # This call and further calls with the same input will be cached
from functools import lru_cache

def cache(func):
    # Using functools.lru_cache to cache function results
    cached_func = lru_cache()(func)

    def wrapper(*args, **kwargs):
        return cached_func(*args, **kwargs)

    # Copying attributes of the original function
    wrapper.__name__ = func.__name__
    wrapper.__doc__ = func.__doc__
    wrapper.__module__ = func.__module__

    return wrapper

# Example usage:
@cache
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

# Test the fibonacci function with caching
print(fibonacci(10))  # This call and further calls with the same input will be cached

In this implementation:

  • lru_cache() is used to create a caching mechanism.
  • The inner wrapper function serves as a wrapper for the original function func, allowing for customization.
  • The attributes of the original function, such as __name__, __doc__, and __module__, are copied to the wrapper function to preserve metadata.
  • The decorator can be applied to any function that needs caching.
  • This @cache decorator will memoize the results of function calls, significantly improving performance for repeated calls with the same arguments.

We can implement a basic cache decorator without using lru_cache. Here’s a simple implementation using a Hash Map aka Dictionary:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def cache(func):
    cache_dict = {}
 
    def wrapper(*args, **kwargs):
        key = (args, frozenset(kwargs.items()))
        if key not in cache_dict:
            cache_dict[key] = func(*args, **kwargs)
        return cache_dict[key]
 
    return wrapper
 
# Example usage:
@cache
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)
 
# Test the fibonacci function with caching
print(fibonacci(10))  # This call and further calls with the same input will be cached
def cache(func):
    cache_dict = {}

    def wrapper(*args, **kwargs):
        key = (args, frozenset(kwargs.items()))
        if key not in cache_dict:
            cache_dict[key] = func(*args, **kwargs)
        return cache_dict[key]

    return wrapper

# Example usage:
@cache
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Test the fibonacci function with caching
print(fibonacci(10))  # This call and further calls with the same input will be cached

In this implementation:

  • cache_dict is used as an in-memory dictionary to store the results of function calls.
  • The wrapper function checks if the arguments and keyword arguments have been seen before. If not, it calls the original function and stores the result in the cache dictionary.
  • The cached result is returned if the same arguments and keyword arguments are provided again.
  • This implementation is a basic form of caching and does not provide features like eviction policies or a maximum cache size, which are available in lru_cache.

–EOF (The Ultimate Computing & Technology Blog) —

GD Star Rating
loading...
904 words
Last Post: Implement and Test the Go Version of Redis Caching Class
Next Post: Teaching Kids Programming - Using Hash Map to Count the Right Triangles in a Binary Matrix

The Permanent URL is: Teaching Kids Programming – The @cache Function Decorator in Python

Leave a Reply