前言
缓存属性( 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
借着这个小章节我们了解下怎么使用以及它的作用(其实看名字你可能已经猜出来):
./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里的实现帮助大家理解一下:
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 的值是多少?
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 '"htmlcode">./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 这个包以外,其他的包都不支持异步函数:
./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功能,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!
昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。
这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。
而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?
更新日志
- 凤飞飞《我们的主题曲》飞跃制作[正版原抓WAV+CUE]
- 刘嘉亮《亮情歌2》[WAV+CUE][1G]
- 红馆40·谭咏麟《歌者恋歌浓情30年演唱会》3CD[低速原抓WAV+CUE][1.8G]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[320K/MP3][193.25MB]
- 【轻音乐】曼托凡尼乐团《精选辑》2CD.1998[FLAC+CUE整轨]
- 邝美云《心中有爱》1989年香港DMIJP版1MTO东芝首版[WAV+CUE]
- 群星《情叹-发烧女声DSD》天籁女声发烧碟[WAV+CUE]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[FLAC/分轨][748.03MB]
- 理想混蛋《Origin Sessions》[320K/MP3][37.47MB]
- 公馆青少年《我其实一点都不酷》[320K/MP3][78.78MB]
- 群星《情叹-发烧男声DSD》最值得珍藏的完美男声[WAV+CUE]
- 群星《国韵飘香·贵妃醉酒HQCD黑胶王》2CD[WAV]
- 卫兰《DAUGHTER》【低速原抓WAV+CUE】
- 公馆青少年《我其实一点都不酷》[FLAC/分轨][398.22MB]
- ZWEI《迟暮的花 (Explicit)》[320K/MP3][57.16MB]