functools.cached_property (Python 3.8)

前言

缓存属性( 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

借着这个小章节我们了解下怎么使用以及它的作用(其实看名字你可能已经猜出来):

上面的例子中首先获得了Foo的实例f,第一次获得 f.bar 时可以看到执行了bar方法的逻辑(因为执行了print语句),之后再获得 f.bar 的值并不会在执行bar方法,而是用了缓存的属性的值。

标准库中的版本还有一种的特点,就是加了线程锁,防止多个线程一起修改缓存。通过对比Werkzeug里的实现帮助大家理解一下:

这个例子中,bar方法对 self.count 做了自增1的操作,然后返回。但是注意f.bar的访问是在10个线程下进行的,里面大家猜现在 f.bar 的值是多少?

结果是10。也就是10个线程同时访问 f.bar ,每个线程中访问时由于都还没有缓存,就会给 f.count 做自增1操作。第三方库对于这个问题可以不关注,只要你确保在项目中不出现多线程并发访问场景即可。但是对于标准库来说,需要考虑的更周全。我们把 cached_property 改成从标准库导入,感受下:

可以看到,由于加了线程锁, f.bar 的结果是正确的1。

cached_property不支持异步

除了 pydanny/cached-property 这个包以外,其他的包都不支持异步函数:

pydanny/cached-property的异步支持实现的很巧妙,我把这部分逻辑抽出来:

我解析一下这段代码:

  1. 对  importasyncio 的异常处理主要为了处理Python 2和Python3.4之前没有asyncio的问题

  2. __get__ 里面会判断方法是不是协程函数,如果是会  returnself._wrap_in_coroutine(obj)

  3. _wrap_in_coroutine 里面首先会把方法封装成一个Task,并把Task对象缓存在  obj.__dict__ 里,wrapper通过装饰器  asyncio.coroutine 包装最后返回。

为了方便理解,在IPython运行一下:

可以看到多次await都可以获得正常结果。如果一个Task对象已经是finished状态,直接返回结果而不会重复执行了。

延伸阅读

  1. https://github.com/bottlepy/bottle/blob/master/bottle.py#L233

  2. https://github.com/pallets/werkzeug/blob/9394af646038abf8b59d6f866a1ea5189f6d46b8/src/werkzeug/utils.py#L53

  3. https://github.com/pypa/pip/blob/873662179aebbf5eacdf681078f47bbfe5ee6149/src/pip/_vendor/distlib/util.py#L437

  4. https://github.com/celery/kombu/blob/080502fd5c4736c0063daa08f5bbd672c3975a68/kombu/utils/objects.py#L5

  5. https://github.com/django/django/blob/master/django/utils/functional.py#L7

  6. https://github.com/pydanny/cached-property

  7. https://github.com/python/cpython/pull/6982