Python 3.8中實現functools.cached_property功能

這篇文章主要介紹了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

如果你犯過他們的代碼其實大同小異,在我的觀點裏面這種輪子是完全沒有必要的。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 '?' for help.
In [1]: f.bar
Out[1]: 10

結果是10。也就是10個線程同時訪問 f.bar ,每個線程中訪問時由於都還沒有緩存,就會給 f.count 做自增1操作。第三方庫對於這個問題可以不關注,只要你確保在項目中不出現多線程併發訪問場景即可。但是對於標準庫來說,需要考慮的更周全。我們把 cached_property 改成從標準庫導入,感受下:

./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: 1

In : 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功能,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回覆大家的。在此也非常感謝大家對神馬文庫網站的支持!
如果你覺得本文對你有幫助,歡迎轉載,煩請註明出處,謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章