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

这次的结果是 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(

将认为是不同的函数调用而缓存在两个缓存节点上。

函数的参数必须可以被

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

def

cache_info():

“”

“报告缓存统计信息”

“”

with

lock:

return

_CacheInfo(

hits,

misses,

maxsize,

cache_len())

def

cache_clear():

“”

“清理缓存信息”

“”

nonlocal

hits,

misses,

full

with

lock:

cache

.

clear()

root[:]

= [

root,

root,

None,

None]

hits

=

misses

=

0

full

=

False

wrapper

.

cache_info

=

cache_info

wrapper

.

cache_clear

=

cache_clear

return

wrapper

如果我写的注释你都看明白了,那也不用看我下面的废话了,如果还有点不太明白,我啰嗦几句,也许你就明白了。

第一、所谓缓存,用的仍然是内存,为了快速存取,用的就是一个 hash 表,也就是 Python 的字典,都是在内存里的操作。

cache

= {}

第二、如果 maxsize == 0,就相当于没有使用缓存,每调用一次,未命中数就 + 1,代码逻辑是这样的:

def
学好 Python 不论是就业还是做副业赚钱都不错,但要学会 Python 还是要有一个学习规划。最后大家分享一份全套的 Python 学习资料,给那些想学习 Python 的小伙伴们一点帮助!

一、Python所有方向的学习路线

Python所有方向路线就是把Python常用的技术点做整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。

二、学习软件

工欲善其事必先利其器。学习Python常用的开发软件都在这里了,给大家节省了很多时间。

三、全套PDF电子书

书籍的好处就在于权威和体系健全,刚开始学习的时候你可以只看视频或者听某个人讲课,但等你学完之后,你觉得你掌握了,这时候建议还是得去看一下书籍,看权威技术书籍也是每个程序员必经之路。

四、入门学习视频

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了。

五、实战案例

光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

六、面试资料

我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值