前言
缓存属性( cached_property )是一个非常常用的功能,很多知名python项目都自己实现过它。我举几个例子:
bottle.cached_property
bottle是我最早接触的web框架,也是我第一次阅读的开源项目源码。最早知道 cached_property 就是通过这个项目,如果你是一个web开发,我不建议你用这个框架,但是源码量少,值得一读~
werkzeug.utils.cached_property
werkzeug是flask的依赖,是应用 cached_property 最成功的一个项目。代码见延伸阅读链接2
pip._vendor.distlib.util.cached_property
pip是python官方包管理工具。代码见延伸阅读链接3
kombu.utils.objects.cached_property
kombu是celery的依赖。代码见延伸阅读链接4
django.utils.functional.cached_property
django是知名web框架,你肯定听过。代码见延伸阅读链接5
甚至有专门的一个包: pydanny/cached-property,延伸阅读6
如果你犯过他们的代码其实大同小异,在我的观点里面这种轮子是完全没有必要的。python 3.8给 functools 模块添加了 cached_property 类,这样就有了官方的实现了
ps: 其实这个issue 2014年就建立了,5年才被merge!
python 3.8的cached_property
借着这个小章节我们了解下怎么使用以及它的作用(其实看名字你可能已经猜出来):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
. / python.exe python 3.8 . 0a4 + (heads / master: 9ee2c264c3 , may 28 2019 , 17 : 44 : 24 ) [clang 10.0 . 0 (clang - 1000.11 . 45.5 )] on darwin type "help" , "copyright" , "credits" or "license" for more information. >>> from functools import cached_property >>> class foo: ... @cached_property ... def bar( self ): ... print ( 'calculate somethings' ) ... return 42 ... >>> f = foo() >>> f.bar calculate somethings 42 >>> f.bar 42 |
上面的例子中首先获得了foo的实例f,第一次获得 f.bar 时可以看到执行了bar方法的逻辑(因为执行了print语句),之后再获得 f.bar 的值并不会在执行bar方法,而是用了缓存的属性的值。
标准库中的版本还有一种的特点,就是加了线程锁,防止多个线程一起修改缓存。通过对比werkzeug里的实现帮助大家理解一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import time from threading import thread from werkzeug.utils import cached_property class foo: def __init__( self ): self .count = 0 @cached_property def bar( self ): time.sleep( 1 ) # 模仿耗时的逻辑,让多线程启动后能执行一会而不是直接结束 self .count + = 1 return self .count threads = [] f = foo() for x in range ( 10 ): t = thread(target = lambda : f.bar) t.start() threads.append(t) for t in threads: t.join() |
这个例子中,bar方法对 self.count 做了自增1的操作,然后返回。但是注意f.bar的访问是在10个线程下进行的,里面大家猜现在 f.bar 的值是多少?
1
2
3
4
5
6
|
ipython - i threaded_cached_property.py python 3.7 . 1 (default, dec 13 2018 , 22 : 28 : 16 ) type 'copyright' , 'credits' or 'license' for more information ipython 7.5 . 0 - - an enhanced interactive python. type '?' for help . in [ 1 ]: f.bar out[ 1 ]: 10 |
结果是10。也就是10个线程同时访问 f.bar ,每个线程中访问时由于都还没有缓存,就会给 f.count 做自增1操作。第三方库对于这个问题可以不关注,只要你确保在项目中不出现多线程并发访问场景即可。但是对于标准库来说,需要考虑的更周全。我们把 cached_property 改成从标准库导入,感受下:
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
27
28
29
30
31
|
. / python.exe python 3.8 . 0a4 + (heads / master: 8cd5165ba0 , may 27 2019 , 22 : 28 : 15 ) [clang 10.0 . 0 (clang - 1000.11 . 45.5 )] on darwin type "help" , "copyright" , "credits" or "license" for more information. >>> import time >>> from threading import thread >>> from functools import cached_property >>> >>> >>> class foo: ... def __init__( self ): ... self .count = 0 ... @cached_property ... def bar( self ): ... time.sleep( 1 ) ... self .count + = 1 ... return self .count ... >>> >>> threads = [] >>> f = foo() >>> >>> for x in range ( 10 ): ... t = thread(target = lambda : f.bar) ... t.start() ... threads.append(t) ... >>> for t in threads: ... t.join() ... >>> f.bar |
可以看到,由于加了线程锁, f.bar 的结果是正确的1。
cached_property不支持异步
除了 pydanny/cached-property 这个包以外,其他的包都不支持异步函数:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
. / python.exe - m asyncio asyncio repl 3.8 . 0a4 + (heads / master: 8cd5165ba0 , may 27 2019 , 22 : 28 : 15 ) [clang 10.0 . 0 (clang - 1000.11 . 45.5 )] on darwin use "await" directly instead of "asyncio.run()" . type "help" , "copyright" , "credits" or "license" for more information. >>> import asyncio >>> from functools import cached_property >>> >>> >>> class foo: ... def __init__( self ): ... self .count = 0 ... @cached_property ... async def bar( self ): ... await asyncio.sleep( 1 ) ... self .count + = 1 ... return self .count ... >>> f = foo() >>> await f.bar 1 >>> await f.bar traceback (most recent call last): file "/users/dongwm/cpython/lib/concurrent/futures/_base.py" , line 439 , in result return self .__get_result() file "/users/dongwm/cpython/lib/concurrent/futures/_base.py" , line 388 , in __get_result raise self ._exception file "<console>" , line 1 , in <module> runtimeerror: cannot reuse already awaited coroutine pydanny / cached - property 的异步支持实现的很巧妙,我把这部分逻辑抽出来: try : import asyncio except (importerror, syntaxerror): asyncio = none class cached_property: def __get__( self , obj, cls ): ... if asyncio and asyncio.iscoroutinefunction( self .func): return self ._wrap_in_coroutine(obj) ... def _wrap_in_coroutine( self , obj): @asyncio .coroutine def wrapper(): future = asyncio.ensure_future( self .func(obj)) obj.__dict__[ self .func.__name__] = future return future return wrapper() |
我解析一下这段代码:
对 import asyncio 的异常处理主要为了处理python 2和python3.4之前没有asyncio的问题
__get__ 里面会判断方法是不是协程函数,如果是会 return self._wrap_in_coroutine(obj)
_wrap_in_coroutine 里面首先会把方法封装成一个task,并把task对象缓存在 obj.__dict__ 里,wrapper通过装饰器 asyncio.coroutine 包装最后返回。
为了方便理解,在ipython运行一下:
in : f = foo()
in : f.bar # 由于用了`asyncio.coroutine`装饰器,这是一个生成器对象
out: <generator object cached_property._wrap_in_coroutine.<locals>.wrapper at 0x10a26f0c0>in : await f.bar # 第一次获得f.bar的值,会sleep 1秒然后返回结果
out: 1in : f.__dict__['bar'] # 这样就把task对象缓存到了f.__dict__里面了,task状态是finished
out: <task finished coro=<foo.bar() done, defined at <ipython-input-54-7f5df0e2b4e7>:4> result=1>in : f.bar # f.bar已经是一个task了
out: <task finished coro=<foo.bar() done, defined at <ipython-input-54-7f5df0e2b4e7>:4> result=1>in : await f.bar # 相当于 await task
out: 1
可以看到多次await都可以获得正常结果。如果一个task对象已经是finished状态,直接返回结果而不会重复执行了。
总结
以上所述是小编给大家介绍的python 3.8中实现functools.cached_property功能,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对服务器之家网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!原文链接:https://www.dongwm.com/post/145