python @lru_cache()

python functools.lru_cache

LRU (Least Recently Used) 是缓存置换策略中的一种常用的算法。当缓存队列已满时,新的元素加入队列时,需要从现有队列中移除一个元素,LRU 策略就是将最近最少被访问的元素移除,从而腾出空间给新的元素。

lru_cache(maxsize=128, typed=False)

lru_cache 装饰器会记录以往函数运行的结果,实现了备忘(memoization)功能,避免参数重复时反复调用,达到提高性能的作用,在递归函数中作用特别明显。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。

maxsize 为最多缓存的次数,如果为 None,则无限制,设置为 2 的 n 次幂时,性能最佳;
如果 typed = True,则不同参数类型的调用将分别缓存,例如 f(3) 和 f(3.0),默认 False

from functools import lru_cache

@lru_cache()
def f1(a, b):
    print("进入函数")
    return (a, b)

@lru_cache(maxsize=256, typed=True)
def f2(a, b):
    print("进入函数")
    return (a, b)


print(f1(3, 'a'))
print(f1(2, 'b'))
print(f1(3.0, 'a'))
print("*" * 40)
print(f2(3, 'a'))
print(f2(2, 'b'))
print(f2(3.0, 'a'))

"""
进入函数
(3, 'a')
进入函数
(2, 'b')
(3, 'a') # 少调用一次函数
****************************************
进入函数
(3, 'a')
进入函数
(2, 'b')
进入函数
(3.0, 'a')
"""

实例:爬虫去重操作,避免网页的重复请求。

from functools import lru_cache
from  requests_html import HTMLSession

session=HTMLSession()

@lru_cache()
def get_html(url):
    req=session.get(url)
    print(url)
    return req

urllist=["https://www.baidu.com","https://pypi.org/project/pylru/1.0.9/","https://www.baidu.com"]

if __name__ == '__main__':
    for i in urllist:
        print(get_html(i))
"""
运行结果:
https://www.baidu.com
<Response [200]>
https://pypi.org/project/pylru/1.0.9/
<Response [200]>
<Response [200]>
"""

python 自带缓存 lru_cache 用法及扩展

一、lru_cache 的使用

1、参数

maxsize 表示被装饰的方法最大可缓存结果数量。如果 typed 设置为 true,不同类型的函数参数将被分别缓存。

def lru_cache(maxsize=128, typed=False):
    if isinstance(maxsize, int):
        if maxsize < 0: maxsize = 0
    elif maxsize is not None:
        raise TypeError('Expected maxsize to be an integer or None')

    def decorating_function(user_function):
        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
        return update_wrapper(wrapper, user_function)

    return decorating_function

2、基本用法

在编写接口时可能需要缓存一些变动不大的数据如配置信息,可能编写如下接口:

@api.route("/user/info", methods=["GET"])
@functools.lru_cache()
@login_require
def get_userinfo_list():
    userinfos = UserInfo.query.all()
    userinfo_list = [user.to_dict() for user in userinfos]
    return jsonify(userinfo_list)

我们缓存了从数据库查询的用户信息,下次再调用这个接口时将直接返回用户信息列表而不需要重新执行一遍数据库查询逻辑,可以有效较少 IO 次数,加快接口反应速度。

3、进阶用法

如果发生用户的删除或者新增时,我们再请求用户接口时仍然返回的是缓存中的数据,这样返回的信息就和我们数据库中的数据就会存在差异,所以当发生用户新增或者删除时,我们需要清除原先的缓存,然后再请求用户接口时可以重新加载缓存。

@api.route("/user/info", methods=["POST"])
@functools.lru_cache()
@login_require
def add_user():
    user = UserInfo(name="李四")
    db.session.add(user)
    db.session.commit()
    
    # 清除get_userinfo_list中的缓存
    get_userinfo_list = current_app.view_functions["api.get_machine_list"]
    cache_info = get_userinfo_list.cache_info()
    # cache_info 具名元组,包含命中次数 hits,未命中次数 misses ,最大缓存数量 maxsize 和 当前缓存大小 currsize
    # 如果缓存数量大于0则清除缓存
    if cache_info[3] > 0:
    	get_userinfo_list.cache_clear()
    return jsonify("新增用户成功")

如果我们把 lru_cache 装饰器和 login_require 装饰器调换位置时,上述的写法将会报错,这是因为 login_require 装饰器中用了 functiontools.wrap 模块进行装饰导致的,具原因我们在下节解释, 如果想不报错得修改成如下写法。

@api.route("/user/info", methods=["POST"])
@login_require
@functools.lru_cache()
def add_user():
    user = UserInfo(name="李四")
    db.session.add(user)
    db.session.commit()
    
    # 清除get_userinfo_list中的缓存
    get_userinfo_list = current_app.view_functions["api.get_machine_list"]
    cache_info = get_userinfo_list.__wrapped__.cache_info()
    # cache_info 具名元组,包含命中次数 hits,未命中次数 misses ,最大缓存数量 maxsize 和 当前缓存大小 currsize
    # 如果缓存数量大于0则清除缓存
    if cache_info[3] > 0:
    	get_userinfo_list.__wrapped__.cache_clear()
    return jsonify("新增用户成功")

二、functiontools.wrap 装饰器对 lru_cache 的影响

@login_require 和 @functools.lru_cache() 装饰器的顺序不同, 就导致了程序是否报错, 其中主要涉及到两点:

login_require 装饰器中是否用了 @functiontools.wrap() 装饰器
@login_require 和 @functools.lru_cache() 装饰器的执行顺序问题

1、多个装饰器装饰同一函数时的执行顺序

def decorator_a(func):
    print('Get in decorator_a')
    def inner_a(*args,**kwargs):
        print('Get in inner_a')
        res = func(*args,**kwargs)
        return res
    return inner_a

def decorator_b(func):
    print('Get in decorator_b')
    def inner_b(*args,**kwargs):
        print('Get in inner_b')
        res = func(*args,**kwargs)
        return res
    return inner_b


@decorator_b
@decorator_a
def f(x):
    print('Get in f')
    return x * 2

f(1)

输出结果如下:

'Get in decorator_a'
'Get in decorator_b'
'Get in inner_b'
'Get in inner_a'
'Get in f'

是不是很像 django 中的中间件的执行顺序,其实原理都差不多。

2、functiontools.wrap 原理

Python 装饰器(decorator)在实现的时候,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变),为了不影响,Python 的 functools 包中提供了一个叫 wraps 的 decorator 来消除这样的副作用。写一个 decorator 的时候,最好在实现之前加上 functools 的 wrap,它能保留原有函数的名称和 docstring。

补充:为了访问原函数此函数会设置一个__wrapped__属性指向原函数。