How to use Python's built-in cache decorators: @lru_cache, @cache and @cached_property

1. Why do you need caching technology?

Using cache is one of the important ways to optimize the speed of Python programs . If used properly, it can greatly reduce the load on computing resources and effectively speed up code execution

Python's built-in library functools module comes with @lru_cache,@cache, @cached_property a decorator, which is very easy to use. It does not need to install third-party libraries, and does not need to store objects in databases such as redis. Usually, only one line of code is needed to perform calculations on function calculation results and class member methods. The results are cached.

This article will introduce the usage steps and examples of these three caching tools.

2. @lru_cacheThe use of cache decorators

@lru_cache is the most common cache decorator. lru_cacheIs the shorthand for: Last recently used cacheit can cache the input parameters and results of the function's most recent call. If there is a new call, first check whether the cache has the same input parameters, and if so, return the corresponding result directly. If it is a function without parameters, after the first call, every subsequent call will directly return the cached result.

First look at an example

from functools import lru_cache
from math import sin

@lru_cache
def sin_half(x):
    return sin(x)/2

print('first call result:',sin_half(60))
print('second call result:',sin_half(60))

In the above example, after the function is run for the first time, lru_cache will cache the call parameters and return results. When running for the second time, lru_cache will check the input and find that the same input parameter 60 exists in the cache, then return the result from the cache. If the function performs a computationally heavy task, for the same input, it can save system resources significantly.

decorator parameters

lru_cache does not clear the cache content by default, so the cache will grow infinitely. If the program is a long-running service, there may be a risk of running out of memory. Therefore, a maxsize parameter must be added:
@lru_cache(maxsize) The parameter maxsize indicates the number of recent calls to be cached.
As @lru_cache(360) indicates, only the last 360 function call results are cached.

@lru_cache(360)
def sin_half(x):
    return sin(x)/2

cache operation

View the cache report: sin_half.cache_info()
Force clear cache content: sin_half.cache_clear()
Of course, you can also use python's garbage collection to clear the cache.

The following uses 1 example to demonstrate the above:

import functools
import gc

# 主要功能: 
# 验证  @lru_cache 装饰器,.chche_info() 和 .cache_clear() 方法的使用
#       garbage collection 的使用

@functools.lru_cache(maxsize = 300) # Max number of Last recently used cache
def fib(n):
	if n < 2:
		return n
	return fib(n-1) + fib(n-2)


fib(30)
fib.cache_clear()

# Before Clearing
print(fib.cache_info())

# After Clearing
print(fib.cache_info())

@functools.lru_cache(maxsize = None)
def gfg1():
    # insert function logic here
    pass

# 再次运行函数 
gfg1()
fib(30)
# garbage collection
gc.collect()

# All objects collected
objects = [i for i in gc.get_objects() 
           if isinstance(i, functools._lru_cache_wrapper)]

print(gfg1.cache_info())

# All objects cleared
for object in objects:
    object.cache_clear()
    
print(gfg1.cache_info())

run level, the output is:

CacheInfo(hits=0, misses=0, maxsize=300, currsize=0)
CacheInfo(hits=0, misses=0, maxsize=300, currsize=0)
CacheInfo(hits=0, misses=1, maxsize=None, currsize=1)
CacheInfo(hits=0, misses=0, maxsize=None, currsize=0)

3. @cacheUse of cache decorators

Compared with @lru_cache, the @cache decorator is lighter, faster, and thread-safe. Different threads can call the same function, and the cached value can be shared.

import functools
import time

@functools.cache
def fib(n):
	if n < 2:
		return n
	return fib(n-1) + fib(n-2)
   
if __name__ == '__main__':
    
    start_time = time.time()
    print(fib(400))
    end_time = time.time()
    execution_time_without_cache = end_time - start_time
    print("Time taken without cache: {:.8f} seconds".format(execution_time_without_cache))
    
    start_time = time.time()
    print(fib(400))
    end_time = time.time()
    execution_time_without_cache = end_time - start_time
    print("Time taken with cache: {:.8f} seconds".format(execution_time_without_cache))    

output:

176023680645013966468226945392411250770384383304492191886725992896575345044216019675
Time taken without cache: 0.00095391 seconds
176023680645013966468226945392411250770384383304492191886725992896575345044216019675
Time taken with cache: 0.00000000 seconds

4. @cached_propertyUse of cache decorators

@cached_propertyis a decorator that converts a method of a class into a property whose value is computed only once and then cached as a normal property. Therefore, the cached result is available as long as the instance persists, and we can use this method as a property of the class, as in

调用:    : instance.method
取代旧方式 : instance.method()

@cached_propertyPart of the functools module in Python. It's similar to property()but @cached_propertywith one extra feature, caching.

But how does it reduce the execution time and make the program faster? Consider the following example:

# Without using @cached_property

# A sample class
class Sample():

	def __init__(self, lst):
	self.long_list = lst

	# a method to find the sum of the
	# given long list of integer values
	def find_sum(self):
		return (sum(self.long_list))

# obj is an instance of the class sample
# the list can be longer, this is just
# an example
obj = Sample([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
print(obj.find_sum())
print(obj.find_sum())
print(obj.find_sum())

The output is:

55
55
55

Here, suppose if we pass 1 long list, the sum of the given list will be calculated every time the find_sum() method is called, thus taking a lot of time to run and the program will eventually slow down. What we want is that since the list doesn't change after the instance is created, it would be nice if the sum of the list would only be calculated once instead of every time we call the method and want to access the sum. This can be achieved by using @cached_property,

# With using @cached_property

from functools import cached_property

# A sample class
class Sample():
	def __init__(self, lst):
	self.long_list = lst

	# a method to find the sum of the
	# given long list of integer values
	@cached_property
	def find_sum(self):
		return (sum(self.long_list))

# obj is an instance of the class sample
obj = Sample([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
print(obj.find_sum)
print(obj.find_sum)
print(obj.find_sum)

After using the @cached_property decorator, there is no need to recalculate, and the find_sum value is always returned immediately.
If you need to recalculate the attribute, just delete the attribute, and the next call will recalculate. In this example, add the following statement,

del obj.find_sum
obj.long_list = range(20,30)
print(obj.find_sum)

Run the code, the output is:

55
55
55
245

5. Applicable scenarios of various caching methods

To sum up, the suggestions are as follows:
1) If the program size is small, or multi-threaded programming, you can use the @cache decorator.
2) If the scale is large and runs for a long time, it is recommended to use the @lru_cache decorator, which is flexible to use, but pay attention to controlling the number of caches, and clean them up manually if necessary.
3) When writing class code, if you need to access the result of a complex operation like a property, use the @cached_property decorator.

Guess you like

Origin blog.csdn.net/captain5339/article/details/131434063