Python 3.8中实现functools.cached_property功能


Posted in Python onMay 29, 2019

前言

缓存属性( 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功能,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Python 相关文章推荐
Python中的if、else、elif语句用法简明讲解
Mar 11 Python
Python中的异常处理相关语句基础学习笔记
Jul 11 Python
详解python如何调用C/C++底层库与互相传值
Aug 10 Python
Python中shape计算矩阵的方法示例
Apr 21 Python
python 统计数组中元素出现次数并进行排序的实例
Jul 02 Python
python basemap 画出经纬度并标定的实例
Jul 09 Python
python实现微信自动回复机器人功能
Jul 11 Python
python 矢量数据转栅格数据代码实例
Sep 30 Python
pytorch实现seq2seq时对loss进行mask的方式
Feb 18 Python
简单了解pytest测试框架setup和tearDown
Apr 14 Python
升级keras解决load_weights()中的未定义skip_mismatch关键字问题
Jun 12 Python
python中scipy.stats产生随机数实例讲解
Feb 19 Python
Python3+Pycharm+PyQt5环境搭建步骤图文详解
May 29 #Python
Python安装与基本数据类型教程详解
May 29 #Python
python登录WeChat 实现自动回复实例详解
May 28 #Python
Python语言进阶知识点总结
May 28 #Python
python图像和办公文档处理总结
May 28 #Python
python网络应用开发知识点浅析
May 28 #Python
python进程和线程用法知识点总结
May 28 #Python
You might like
关于IIS php调用com组件的权限问题
2012/01/11 PHP
ThinkPHP框架里隐藏index.php
2016/04/12 PHP
php 使用curl模拟登录人人(校内)网的简单实例
2016/06/06 PHP
PHP经典算法集锦【经典收藏】
2016/09/14 PHP
PHPExcel导出2003和2007的excel文档功能示例
2017/01/04 PHP
Javascript实现DIV滚动自动滚动到底部的代码
2012/03/01 Javascript
js实现简单的购物车有图有代码
2014/05/26 Javascript
JavaScript  cookie 跨域访问之广告推广
2016/04/20 Javascript
javascript中异常处理案例(推荐)
2016/10/03 Javascript
javascript另类方法实现htmlencode()与htmldecode()函数实例分析
2016/11/17 Javascript
详解vue.js组件化开发实践
2016/12/14 Javascript
js获取浏览器的各种属性
2017/04/27 Javascript
在一般处理程序(ashx)中弹出js提示语
2017/08/16 Javascript
vue.js或js实现中文A-Z排序的方法
2018/03/08 Javascript
使用electron实现百度网盘悬浮窗口功能的示例代码
2018/10/24 Javascript
vue2.0中set添加属性后视图不能更新的解决办法
2019/02/22 Javascript
vue下载excel的实现代码后台用post方法
2019/05/10 Javascript
Python 修改列表中的元素方法
2018/06/26 Python
linux安装Python3.4.2的操作方法
2018/09/28 Python
用Python写一个模拟qq聊天小程序的代码实例
2019/03/06 Python
python如何获取列表中每个元素的下标位置
2019/07/01 Python
Django Form 实时从数据库中获取数据的操作方法
2019/07/25 Python
python的slice notation的特殊用法详解
2019/12/27 Python
pycharm-professional-2020.1下载与激活的教程
2020/09/21 Python
Python实现邮件发送的详细设置方法(遇到问题)
2021/01/18 Python
基于CSS3制作立体效果导航菜单
2016/01/12 HTML / CSS
详解通过HTML5 Canvas实现图片的平移及旋转变化的方法
2016/03/22 HTML / CSS
英国香水店:The Perfume Shop
2017/03/27 全球购物
门卫人员岗位职责
2013/12/24 职场文书
如何写一份好的自荐信
2014/01/02 职场文书
违反学校规定检讨书
2014/01/18 职场文书
乡镇综治宣传月活动总结
2014/07/02 职场文书
2014年小学生迎国庆65周年演讲稿
2014/09/27 职场文书
2015年导购员工作总结
2015/04/25 职场文书
2016年党员干部公开承诺书
2016/03/24 职场文书
pytorch显存一直变大的解决方案
2021/04/08 Python