侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

Python 3.8中实现functools.cached_property功能

Python  /  管理员 发布于 7年前   204

前言

缓存属性( 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.exePython 3.8.0a4+ (heads/master:9ee2c264c3, May 28 2019, 17:44:24)[Clang 10.0.0 (clang-1000.11.45.5)] on darwinType "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.barcalculate somethings42>>> f.bar42

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

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

import timefrom threading import Threadfrom werkzeug.utils import cached_propertyclass Foo:  def __init__(self):    self.count = 0  @cached_property  def bar(self):    time.sleep(1) # 模仿耗时的逻辑,让多线程启动后能执行一会而不是直接结束    self.count += 1    return self.countthreads = []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.pyPython 3.7.1 (default, Dec 13 2018, 22:28:16)Type 'copyright', 'credits' or 'license' for more informationIPython 7.5.0 -- An enhanced Interactive Python. Type '?' for help.In [1]: f.barOut[1]: 10

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

./python.exePython 3.8.0a4+ (heads/master:8cd5165ba0, May 27 2019, 22:28:15)[Clang 10.0.0 (clang-1000.11.45.5)] on darwinType "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 asyncioasyncio REPL 3.8.0a4+ (heads/master:8cd5165ba0, May 27 2019, 22:28:15)[Clang 10.0.0 (clang-1000.11.45.5)] on darwinUse "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.bar1>>> await f.barTraceback (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 coroutinepydanny/cached-property的异步支持实现的很巧妙,我把这部分逻辑抽出来:try:  import asyncioexcept (ImportError, SyntaxError):  asyncio = Noneclass 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 odoo中嵌入html简单的分页功能
    下一条:
    Python3+Pycharm+PyQt5环境搭建步骤图文详解
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在python语言中Flask框架的学习及简单功能示例(0个评论)
    • 在Python语言中实现GUI全屏倒计时代码示例(0个评论)
    • Python + zipfile库实现zip文件解压自动化脚本示例(0个评论)
    • python爬虫BeautifulSoup快速抓取网站图片(1个评论)
    • vscode 配置 python3开发环境的方法(0个评论)
    • 近期文章
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • PHP 8.4 Alpha 1现已发布!(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2018-04
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2022-01
    • 2023-07
    • 2023-10
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客