前言
緩存屬性( cached_property )是一個(gè)非常常用的功能,很多知名Python項(xiàng)目都自己實(shí)現(xiàn)過它。我舉幾個(gè)例子:
bottle.cached_property
Bottle是我最早接觸的Web框架,也是我第一次閱讀的開源項(xiàng)目源碼。最早知道 cached_property 就是通過這個(gè)項(xiàng)目,如果你是一個(gè)Web開發(fā),我不建議你用這個(gè)框架,但是源碼量少,值得一讀~
werkzeug.utils.cached_property
Werkzeug是Flask的依賴,是應(yīng)用 cached_property 最成功的一個(gè)項(xiàng)目。代碼見延伸閱讀鏈接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
甚至有專門的一個(gè)包: pydanny/cached-property ,延伸閱讀6
如果你犯過他們的代碼其實(shí)大同小異,在我的觀點(diǎn)里面這種輪子是完全沒有必要的。Python 3.8給 functools 模塊添加了 cached_property 類,這樣就有了官方的實(shí)現(xiàn)了
PS: 其實(shí)這個(gè)Issue 2014年就建立了,5年才被Merge!
Python 3.8的cached_property
借著這個(gè)小章節(jié)我們了解下怎么使用以及它的作用(其實(shí)看名字你可能已經(jīng)猜出來):
./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的實(shí)例f,第一次獲得 f.bar 時(shí)可以看到執(zhí)行了bar方法的邏輯(因?yàn)閳?zhí)行了print語句),之后再獲得 f.bar 的值并不會(huì)在執(zhí)行bar方法,而是用了緩存的屬性的值。
標(biāo)準(zhǔn)庫(kù)中的版本還有一種的特點(diǎn),就是加了線程鎖,防止多個(gè)線程一起修改緩存。通過對(duì)比Werkzeug里的實(shí)現(xiàn)幫助大家理解一下:
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) # 模仿耗時(shí)的邏輯,讓多線程啟動(dòng)后能執(zhí)行一會(huì)而不是直接結(jié)束 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()
這個(gè)例子中,bar方法對(duì) self.count 做了自增1的操作,然后返回。但是注意f.bar的訪問是在10個(gè)線程下進(jìn)行的,里面大家猜現(xiàn)在 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 '?' for help. In [1]: f.bar Out[1]: 10
結(jié)果是10。也就是10個(gè)線程同時(shí)訪問 f.bar ,每個(gè)線程中訪問時(shí)由于都還沒有緩存,就會(huì)給 f.count 做自增1操作。第三方庫(kù)對(duì)于這個(gè)問題可以不關(guān)注,只要你確保在項(xiàng)目中不出現(xiàn)多線程并發(fā)訪問場(chǎng)景即可。但是對(duì)于標(biāo)準(zhǔn)庫(kù)來說,需要考慮的更周全。我們把 cached_property 改成從標(biāo)準(zhǔn)庫(kù)導(dǎo)入,感受下:
./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 的結(jié)果是正確的1。
cached_property不支持異步
除了 pydanny/cached-property 這個(gè)包以外,其他的包都不支持異步函數(shù):
./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 "", line 1, in RuntimeError: cannot reuse already awaited coroutine pydanny/cached-property的異步支持實(shí)現(xiàn)的很巧妙,我把這部分邏輯抽出來: 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()
我解析一下這段代碼:
對(duì) import asyncio 的異常處理主要為了處理Python 2和Python3.4之前沒有asyncio的問題
__get__ 里面會(huì)判斷方法是不是協(xié)程函數(shù),如果是會(huì) return self._wrap_in_coroutine(obj)
_wrap_in_coroutine 里面首先會(huì)把方法封裝成一個(gè)Task,并把Task對(duì)象緩存在 obj.__dict__ 里,wrapper通過裝飾器 asyncio.coroutine 包裝最后返回。
為了方便理解,在IPython運(yùn)行一下:
In : f = Foo()
In : f.bar? # 由于用了`asyncio.coroutine`裝飾器,這是一個(gè)生成器對(duì)象
Out:.wrapper at 0x10a26f0c0> In : await f.bar? # 第一次獲得f.bar的值,會(huì)sleep 1秒然后返回結(jié)果
Out: 1In : f.__dict__['bar']? # 這樣就把Task對(duì)象緩存到了f.__dict__里面了,Task狀態(tài)是finished
Out::4> result=1> In : f.bar? # f.bar已經(jīng)是一個(gè)task了
Out::4> result=1> In : await f.bar? # 相當(dāng)于 await task
Out: 1
可以看到多次await都可以獲得正常結(jié)果。如果一個(gè)Task對(duì)象已經(jīng)是finished狀態(tài),直接返回結(jié)果而不會(huì)重復(fù)執(zhí)行了。
總結(jié)
以上所述是小編給大家介紹的Python 3.8中實(shí)現(xiàn)functools.cached_property功能,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請(qǐng)注明出處,謝謝!
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
