站长网 语言 Python 标准库中很有用的装饰器

Python 标准库中很有用的装饰器

众所周知,Python 语言灵活、简洁,对程序员友好,但在性能上有点不太令人满意,这一点通过一个递归的求斐波那契额函数就可以说明:def fib(n): if n = 1: return n return fib(n – 1) + fib(n – 2)在我的 MBP 上计算 fib(40) 花费了 33 秒:import timedef

众所周知,Python 语言灵活、简洁,对程序员友好,但在性能上有点不太令人满意,这一点通过一个递归的求斐波那契额函数就可以说明:

 

def fib(n): 

    if n <= 1: 

        return n 

    return fib(n – 1) + fib(n – 2) 

在我的 MBP 上计算 fib(40) 花费了 33 秒:

 

import time 

 

def main(): 

    start = time.time() 

    result = fib(40) 

    end = time.time() 

    cost = end – start 

    print(f"{result = } {cost = :.4f}") 

 

if __name__ == '__main__': 

    main() 

 

 

但是,假如使用标准库中的这个装饰器,那结果完全不一样

 

from functools import lru_cache 

 

@lru_cache 

def fib(n): 

    if n <= 1: 

        return n 

    return fib(n – 1) + fib(n – 2) 

这次的结果是 0 秒,你没看错,我保留了 4 位小数,后面的忽略了。

 

 

 

提升了多少倍?我已经计算不出来了。

 

为什么 lru_cache 装饰器这么牛逼,它到底做了什么事情?今天就来聊一聊这个最有用的装饰器。

 

如果看过计算机操作系统的话,你对 LRU 一定不会陌生,这就是著名的最近最久未使用缓存淘汰算法。

 

而 lru_cache 就是这个算法的具体实现。(这个算法可是面试经常考的哦,有的面试官要求现场手写代码)

 

现在,我们来看一个 lru_cache 的源代码,其中的英文注释,我已经为你翻译为中文:

 

def lru_cache(maxsize=128, typed=False): 

    """LRU 缓存装饰器 

 

    如果 *maxsize* 是 None, 将不会淘汰缓存,缓存大小也不做限制 

 

    如果 *typed* 是 True, 不同类型的参数将独立做缓存,比如 f(3.0) and f(3) 将认为是不同的函数调用而缓存在两个缓存节点上。 

 

    函数的参数必须可以被 hash 

 

    查看缓存信息使用的是命名元组 (hits, misses, maxsize, currsize) 

    查看缓存信息:user_func.cache_info().  清理缓存信息:user_func.cache_clear(). 

 

    LRU 算法:  http://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU) 

 

    """ 

 

    # lru_cache 的内部实现是线程安全的 

 

    if isinstance(maxsize, int): 

        # 负数转换为 0  

        if maxsize < 0: 

            maxsize = 0 

    elif callable(maxsize) and isinstance(typed, bool): 

        #如果被装饰的函数(user_function)直接通过 maxsize 参数传入  

        user_function, maxsize = maxsize, 128 

        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo) 

        return update_wrapper(wrapper, user_function) 

    elif maxsize is not None: 

        raise TypeError( 

            'Expected first argument to be an integer, a callable, 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 

这里面有两个参数,一个是 maxsize,表示缓存的大小,当传入负数时,自动设置为 0,如果不传入 maxsize,或者设置为 None,表示缓存没有大小限制,此时没有缓存淘汰。还有一个是 type,当 type 传入 True 时,不同的参数类型会当作不同的 key 存到缓存当中。

 

接下来,lru_cache 的核心在这个函数上 _lru_cache_wrapper,建议有感情的阅读、背诵并默写。我们来看下它的源代码

 

def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo): 

    # 所有 lru cache 实例共享的常量: 

    sentinel = object()          # 用来表示缓存未命中的唯一对象 

    make_key = _make_key         # build a key from the function arguments 

    PREV, NEXT, KEY, RESULT = 0, 1, 2, 3   # names for the link fields 

 

    cache = {} 

    hits = misses = 0 

    full = False 

    cache_get = cache.get    # 绑定函数来获取缓存中 key 的值 

    cache_len = cache.__len__  # 绑定函数获取缓存大小 

    lock = RLock()           # 因为链表上的更新是线程不安全的 

    root = []                # 循环双向链表的根节点 

    root[:] = [root, root, None, None]     # 初始化根节点的前后指针都指向它自己 

 

    if maxsize == 0: 

 

        def wrapper(*args, **kwds): 

            # 没有缓存,仅更新统计信息 

            nonlocal misses 

            misses += 1 

            result = user_function(*args, **kwds) 

            return result 

 

    elif maxsize is None: 

 

        def wrapper(*args, **kwds): 

            # 仅仅排序,不考虑排序和缓存大小限制 

            nonlocal hits, misses 

            key = make_key(args, kwds, typed) 

            result = cache_get(key, sentinel) 

            if result is not sentinel: 

                hits += 1 

                return result 

            misses += 1 

            result = user_function(*args, **kwds) 

            cache[key] = result 

            return result 

 

    else: 

 

        def wrapper(*args, **kwds): 

            # 大小有限制,并跟踪最近使用的缓存 

            nonlocal root, hits, misses, full 

            key = make_key(args, kwds, typed) 

            with lock: 

                link = cache_get(key) 

                if link is not None: 

                    # 缓存命中,将命中的缓存移动到循环双向链表的头部 

                    link_prev, link_next, _key, result = link 

                    link_prev[NEXT] = link_next 

                    link_next[PREV] = link_prev 

                    last = root[PREV] 

                    last[NEXT] = root[PREV] = link 

                    link[PREV] = last 

                    link[NEXT] = root 

                    hits += 1 

                    return result 

                misses += 1 

            result = user_function(*args, **kwds) 

            with lock: 

                if key in cache: 

                    # 走到这里说明 key 已经放在了缓存,且锁已经释放了,链表已经更新了,这里什么也不需要做了,最后只需要返回计算的结果就可以了。 

                    pass 

                elif full: 

                    # 如果缓存满了, 使用最老的根节点来存储新节点就可以了,链表上不需要删除(是不是很聪明) 

                    oldroot = root 

                    oldroot[KEY] = key 

                    oldroot[RESULT] = result 

                    root = oldroot[NEXT] 

                    oldkey = root[KEY] 

                    oldresult = root[RESULT] 

                    root[KEY] = root[RESULT] = None 

                     

                    # 最后,我们需要从缓存中清除这个 key,因为它已经无效了。 

                    del cache[oldkey] 

                    # 新值放入缓存 

                    cache[key] = oldroot 

                else: 

                    # 如果没有满,将新的结果放入循环双向链表的头部 

                    last = root[PREV] 

                    link = [last, root, key, result] 

                    last[NEXT] = root[PREV] = cache[key] = link 

                    # 使用 cache_len 绑定方法而不是 len() 函数,后者可能会被包装在 lru_cache 本身中 

                    full = (cache_len() >= maxsize) 

            return result 

本文来自网络,不代表站长网立场,转载请注明出处:https://www.tzzz.com.cn/html/biancheng/yuyan/2021/1102/18049.html

作者: dawei

【声明】:站长网内容转载自互联网,其相关言论仅代表作者个人观点绝非权威,不代表本站立场。如您发现内容存在版权问题,请提交相关链接至邮箱:bqsm@foxmail.com,我们将及时予以处理。
联系我们

联系我们

0577-28828765

在线咨询: QQ交谈

邮箱: xwei067@foxmail.com

工作时间:周一至周五,9:00-17:30,节假日休息

返回顶部