Python 3.8中实现functools.cached_property功能
前言
缓存属性(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
如果你犯过他们的代码其实大同小异,在我的观点里面这种轮子是完全没有必要的。Python3.8给functools模块添加了cached_property类,这样就有了官方的实现了
PS:其实这个Issue2014年就建立了,5年才被Merge!
Python3.8的cached_property
借着这个小章节我们了解下怎么使用以及它的作用(其实看名字你可能已经猜出来):
./python.exe Python3.8.0a4+(heads/master:9ee2c264c3,May282019,17:44:24) [Clang10.0.0(clang-1000.11.45.5)]ondarwin Type"help","copyright","credits"or"license"formoreinformation. >>>fromfunctoolsimportcached_property >>>classFoo: ...@cached_property ...defbar(self): ...print('calculatesomethings') ...return42 ... >>>f=Foo() >>>f.bar calculatesomethings 42 >>>f.bar 42
上面的例子中首先获得了Foo的实例f,第一次获得f.bar时可以看到执行了bar方法的逻辑(因为执行了print语句),之后再获得f.bar的值并不会在执行bar方法,而是用了缓存的属性的值。
标准库中的版本还有一种的特点,就是加了线程锁,防止多个线程一起修改缓存。通过对比Werkzeug里的实现帮助大家理解一下:
importtime fromthreadingimportThread fromwerkzeug.utilsimportcached_property classFoo: def__init__(self): self.count=0 @cached_property defbar(self): time.sleep(1)#模仿耗时的逻辑,让多线程启动后能执行一会而不是直接结束 self.count+=1 returnself.count threads=[] f=Foo() forxinrange(10): t=Thread(target=lambda:f.bar) t.start() threads.append(t) fortinthreads: t.join()
这个例子中,bar方法对self.count做了自增1的操作,然后返回。但是注意f.bar的访问是在10个线程下进行的,里面大家猜现在f.bar的值是多少?
ipython-ithreaded_cached_property.py Python3.7.1(default,Dec132018,22:28:16) Type'copyright','credits'or'license'formoreinformation IPython7.5.0--AnenhancedInteractivePython.Type'?'forhelp. In[1]:f.bar Out[1]:10
结果是10。也就是10个线程同时访问f.bar,每个线程中访问时由于都还没有缓存,就会给f.count做自增1操作。第三方库对于这个问题可以不关注,只要你确保在项目中不出现多线程并发访问场景即可。但是对于标准库来说,需要考虑的更周全。我们把cached_property改成从标准库导入,感受下:
./python.exe Python3.8.0a4+(heads/master:8cd5165ba0,May272019,22:28:15) [Clang10.0.0(clang-1000.11.45.5)]ondarwin Type"help","copyright","credits"or"license"formoreinformation. >>>importtime >>>fromthreadingimportThread >>>fromfunctoolsimportcached_property >>> >>> >>>classFoo: ...def__init__(self): ...self.count=0 ...@cached_property ...defbar(self): ...time.sleep(1) ...self.count+=1 ...returnself.count ... >>> >>>threads=[] >>>f=Foo() >>> >>>forxinrange(10): ...t=Thread(target=lambda:f.bar) ...t.start() ...threads.append(t) ... >>>fortinthreads: ...t.join() ... >>>f.bar
可以看到,由于加了线程锁,f.bar的结果是正确的1。
cached_property不支持异步
除了pydanny/cached-property这个包以外,其他的包都不支持异步函数:
./python.exe-masyncio asyncioREPL3.8.0a4+(heads/master:8cd5165ba0,May272019,22:28:15) [Clang10.0.0(clang-1000.11.45.5)]ondarwin Use"await"directlyinsteadof"asyncio.run()". Type"help","copyright","credits"or"license"formoreinformation. >>>importasyncio >>>fromfunctoolsimportcached_property >>> >>> >>>classFoo: ...def__init__(self): ...self.count=0 ...@cached_property ...asyncdefbar(self): ...awaitasyncio.sleep(1) ...self.count+=1 ...returnself.count ... >>>f=Foo() >>>awaitf.bar 1 >>>awaitf.bar Traceback(mostrecentcalllast): File"/Users/dongwm/cpython/Lib/concurrent/futures/_base.py",line439,inresult returnself.__get_result() File"/Users/dongwm/cpython/Lib/concurrent/futures/_base.py",line388,in__get_result raiseself._exception File"",line1,in RuntimeError:cannotreusealreadyawaitedcoroutine pydanny/cached-property的异步支持实现的很巧妙,我把这部分逻辑抽出来: try: importasyncio except(ImportError,SyntaxError): asyncio=None classcached_property: def__get__(self,obj,cls): ... ifasyncioandasyncio.iscoroutinefunction(self.func): returnself._wrap_in_coroutine(obj) ... def_wrap_in_coroutine(self,obj): @asyncio.coroutine defwrapper(): future=asyncio.ensure_future(self.func(obj)) obj.__dict__[self.func.__name__]=future returnfuture returnwrapper()
我解析一下这段代码:
对importasyncio的异常处理主要为了处理Python2和Python3.4之前没有asyncio的问题
__get__里面会判断方法是不是协程函数,如果是会returnself._wrap_in_coroutine(obj)
_wrap_in_coroutine里面首先会把方法封装成一个Task,并把Task对象缓存在obj.__dict__里,wrapper通过装饰器asyncio.coroutine包装最后返回。
为了方便理解,在IPython运行一下:
In:f=Foo()
In:f.bar #由于用了`asyncio.coroutine`装饰器,这是一个生成器对象
Out:.wrapperat0x10a26f0c0> In:awaitf.bar #第一次获得f.bar的值,会sleep1秒然后返回结果
Out:1In:f.__dict__['bar'] #这样就把Task对象缓存到了f.__dict__里面了,Task状态是finished
Out::4>result=1> In:f.bar #f.bar已经是一个task了
Out::4>result=1> In:awaitf.bar #相当于awaittask
Out:1
可以看到多次await都可以获得正常结果。如果一个Task对象已经是finished状态,直接返回结果而不会重复执行了。
总结
以上所述是小编给大家介绍的Python3.8中实现functools.cached_property功能,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!