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

(0)

相关推荐

  • Python中用Descriptor实现类级属性(Property)详解

    上篇文章简单介绍了python中描述器(Descriptor)的概念和使用,有心的同学估计已经Get√了该技能.本篇文章通过一个Descriptor的使用场景再次给出一个案例,让不了解情况的同学可以更容易理解. 先说说decorator 这两个单词确实是有些相似,同时在使用中也是形影不离.这也给人造成了理解上的困难,说装饰器和描述器到底是怎么回事,为什么非得用一个@符号再加上描述器才行. 很多文章也都把这俩结合着讲,我自己看完之后都会觉得很绕.其实学习一个知识点,和做项目开发一个功能是一样的.在

  • Python Property属性的2种用法

    假设定义了一个类:C,该类必须继承自object类,有一私有变量_x 复制代码 代码如下: class C: def __init__(self): self.__x=None 1.现在介绍第一种使用属性的方法: 在该类中定义三个函数,分别用作赋值.取值和删除变量(此处表达也许不很清晰,请看示例) def getx(self): return self.__x def setx(self,value): self.__x=value def delx(self): del self.__x x=

  • 实例讲解Python编程中@property装饰器的用法

    取值和赋值 class Actress(): def __init__(self): self.name = 'TianXin' self.age = 5 类Actress中有两个成员变量name和age.在外部对类的成员变量的操作,主要包括取值和赋值.简单的取值操作是x=object.var,简单的赋值操作是object.var=value. >>> actress = Actress() >>> actress.name #取值操作 'TianXin' >&g

  • 介绍Python的@property装饰器的用法

    在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把成绩随便改: s = Student() s.score = 9999 这显然不合逻辑.为了限制score的范围,可以通过一个set_score()方法来设置成绩,再通过一个get_score()来获取成绩,这样,在set_score()方法里,就可以检查参数: class Student(object): def get_score(self): return self._score def set_s

  • python @property的用法及含义全面解析

    在接触python时最开始接触的代码,取长方形的长和宽,定义一个长方形类,然后设置长方形的长宽属性,通过实例化的方式调用长和宽,像如下代码一样. class Rectangle(object): def __init__(self): self.width =10 self.height=20 r=Rectangle() print(r.width,r.height) 此时输出结果为10 20 但是这样在实际使用中会产生一个严重的问题,__init__ 中定义的属性是可变的,换句话说,是使用一个

  • Python解析json之ValueError: Expecting property name enclosed in double quotes: line 1 column 2(char 1)

    前言 在Python中提供了json包来方便快捷的解析json字串的转换过程,但是碰到了一个比较奇怪的问题,就是不太正确的json串如何来解析? 1. 问题的提出 今天在处理一个http请求的响应json之时,基于python进行解析,碰到了如下错误: Traceback (most recent call last): File "<ipython-input-16-bff463ed9408>", line 1, in <module> json.loads(

  • python的描述符(descriptor)、装饰器(property)造成的一个无限递归问题分享

    分享一下刚遇到的一个小问题,我有一段类似于这样的python代码: 复制代码 代码如下: # coding: utf-8 class A(object): @property     def _value(self): #        raise AttributeError("test")         return {"v": "This is a test."} def __getattr__(self, key):         p

  • Python 3.8中实现functools.cached_property功能

    前言 缓存属性( cached_property )是一个非常常用的功能,很多知名Python项目都自己实现过它.我举几个例子: bottle.cached_property Bottle是我最早接触的Web框架,也是我第一次阅读的开源项目源码.最早知道 cached_property 就是通过这个项目,如果你是一个Web开发,我不建议你用这个框架,但是源码量少,值得一读~ werkzeug.utils.cached_property Werkzeug是Flask的依赖,是应用 cached_p

  • 详解Python 3.10 中的新功能和变化

    随着最后一个alpha版发布,Python 3.10 的功能更改全面敲定! 现在,正是体验Python 3.10 新功能的理想时间!正如标题所言,本文将给大家分享Python 3.10中所有重要的功能和更改. 新功能1:联合运算符 在过去, |符号用于 "算术或"运算,例如: print(0 | 0) print(0 | 1) print({1, 2} | {2, 3}) 输出: 0 1 {1, 2, 3} 在Python 3.10中, |符号有的新语法,可以表示x类型 或 Y类型,以

  • Python模块包中__init__.py文件功能分析

    本文实例讲述了Python模块包中__init__.py文件功能.分享给大家供大家参考,具体如下: 用django做开发已经一年多的时间,但基本没注意python模块中__init__.py文件存在的意义,偶然看到对它的介绍吓一大跳,这个文件太重要了 模块包: 包通常总是一个目录,目录下为首的一个文件便是 __init__.py.然后是一些模块文件和子目录,假如子目录中也有 __init__.py 那么它就是这个包的子包了. 差不多就像这样吧: Package1/ __init__.py   

  • python实现应用程序在右键菜单中添加打开方式功能

    最近项目组开发的一个小工具想要在右键菜单中添加打开方式,以有道云笔记为例进行了需求拆解和代码编写 1.需求拆解: 如何实现手动添加右键菜单的打开方式: Step1:打开注册表编辑器,Win+R->输入 "regedit" Step2:在HKEY_CLASSES_ROOT/*/shell (或者HKEY_LOCAL_MACHINE/SOFTWARE/Classes/*/shell ,两个目录是一样的) 添加一个key:YNote,然后在该项中新建项command,然后再编辑字符串,

  • Ubuntu下使用Python实现游戏制作中的切分图片功能

    本文实例讲述了Ubuntu下使用Python实现游戏制作中的切分图片功能.分享给大家供大家参考,具体如下: why 拿到一个人物行走的素材,要用TexturePacker打包.TexturePacker打包后,助于游戏加载图片效率,且比较好管理. 目前得到一张整图,无法直接导入到TexturePacker. what 切片:使用切片将源图像分成许多的功能区域. how 1 ubuntu下图片处理软件 GIMP: 画好参考线后, 点击 滤镜->WEB ->切片 2 python + PIL  (

  • Python中的探索性数据分析(功能式)

    这里有一些技巧来处理日志文件提取.假设我们正在查看一些Enterprise Splunk提取.我们可以用Splunk来探索数据.或者我们可以得到一个简单的提取并在Python中摆弄这些数据. 在Python中运行不同的实验似乎比试图在Splunk中进行这种探索性的操作更有效.主要是因为我们可以无所限制地对数据做任何事.我们可以在一个地方创建非常复杂的统计模型. 理论上,我们可以在Splunk中做很多的探索.它有各种报告和分析功能. 但是... 使用Splunk需要假设我们知道我们正在寻找什么.在

  • Python实现字符串中某个字母的替代功能

    今晚想实现这样一个功能:将输入字符串中的字母 "i" 变成字母 "p".当时想的很简单,直接用for循环遍历,然后替代,出问题的代码如下: name = input('随便输入一堆字符吧...') #name = list(name) name = "".join(name) j = 0 for i in range(len(name)): if name[i] == 'i': name[i] = 'p' j = j + 1 i = i + 1

  • Python设置在shell脚本中自动补全功能的方法

    本篇博客将会简短的介绍,如何在ubuntu中设置python自动补全功能. 需求:由于python中的内建函数较多,我们在百纳乘时,可能记不清函数的名字,同时自动补全功能,加快了我们开发的效率. 方法以及要求:设置过程中需要看客具备一点ubuntu的基本知识,当然不懂也没有关系,跟着一步一步走,不懂得可以学习一下ubuntu的文件系统. 具体步骤 1.首先创建一个文件:~/.pythonrc 在ubuntu中创建文件的命令为: sudo gedit ~/.pythonrc #sudo表示使用管理

  • Python中的groupby分组功能的实例代码

    pandas中的DataFrame中可以根据某个属性的同一值进行聚合分组,可以选单个属性,也可以选多个属性: 代码示例: import pandas as pd A=pd.DataFrame([['Beijing',1.68,2300,'city','Yes'],['Tianjin',1.13,1293,'city','Yes'],['Shaanxi',20.56,3732,'Province','Yes'],['Hebei',18.77,7185,'Province','No'],['Qing

  • python实现的批量分析xml标签中各个类别个数功能示例

    本文实例讲述了python实现的批量分析xml标签中各个类别个数功能.分享给大家供大家参考,具体如下: 文章目录 需要个脚本分析下各个目标的数目 顺带练习下多进程,自用,直接上代码: # -*- coding: utf-8 -*- # @Time : 2019/06/10 18:56 # @Author : TuanZhangSama import os import xml.etree.ElementTree as ET from multiprocessing import Pool,fre

随机推荐