经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Python » 查看文章
Python 3.8中实现functools.cached_property功能
来源:jb51  时间:2019/5/30 8:42:48  对本文有异议

前言

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

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

  1. ./python.exe
  2. Python 3.8.0a4+ (heads/master:9ee2c264c3, May 28 2019, 17:44:24)
  3. [Clang 10.0.0 (clang-1000.11.45.5)] on darwin
  4. Type "help", "copyright", "credits" or "license" for more information.
  5. >>> from functools import cached_property
  6. >>> class Foo:
  7. ... @cached_property
  8. ... def bar(self):
  9. ... print('calculate somethings')
  10. ... return 42
  11. ...
  12. >>> f = Foo()
  13. >>> f.bar
  14. calculate somethings
  15. 42
  16. >>> f.bar
  17. 42

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

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

  1. import time
  2. from threading import Thread
  3. from werkzeug.utils import cached_property
  4. class Foo:
  5. def __init__(self):
  6. self.count = 0
  7. @cached_property
  8. def bar(self):
  9. time.sleep(1) # 模仿耗时的逻辑,让多线程启动后能执行一会而不是直接结束
  10. self.count += 1
  11. return self.count
  12. threads = []
  13. f = Foo()
  14. for x in range(10):
  15. t = Thread(target=lambda: f.bar)
  16. t.start()
  17. threads.append(t)
  18. for t in threads:
  19. t.join()

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

  1. ipython -i threaded_cached_property.py
  2. Python 3.7.1 (default, Dec 13 2018, 22:28:16)
  3. Type 'copyright', 'credits' or 'license' for more information
  4. IPython 7.5.0 -- An enhanced Interactive Python. Type '?' for help.
  5. In [1]: f.bar
  6. Out[1]: 10

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

  1. ./python.exe
  2. Python 3.8.0a4+ (heads/master:8cd5165ba0, May 27 2019, 22:28:15)
  3. [Clang 10.0.0 (clang-1000.11.45.5)] on darwin
  4. Type "help", "copyright", "credits" or "license" for more information.
  5. >>> import time
  6. >>> from threading import Thread
  7. >>> from functools import cached_property
  8. >>>
  9. >>>
  10. >>> class Foo:
  11. ... def __init__(self):
  12. ... self.count = 0
  13. ... @cached_property
  14. ... def bar(self):
  15. ... time.sleep(1)
  16. ... self.count += 1
  17. ... return self.count
  18. ...
  19. >>>
  20. >>> threads = []
  21. >>> f = Foo()
  22. >>>
  23. >>> for x in range(10):
  24. ... t = Thread(target=lambda: f.bar)
  25. ... t.start()
  26. ... threads.append(t)
  27. ...
  28. >>> for t in threads:
  29. ... t.join()
  30. ...
  31. >>> f.bar

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

cached_property不支持异步

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

  1. ./python.exe -m asyncio
  2. asyncio REPL 3.8.0a4+ (heads/master:8cd5165ba0, May 27 2019, 22:28:15)
  3. [Clang 10.0.0 (clang-1000.11.45.5)] on darwin
  4. Use "await" directly instead of "asyncio.run()".
  5. Type "help", "copyright", "credits" or "license" for more information.
  6. >>> import asyncio
  7. >>> from functools import cached_property
  8. >>>
  9. >>>
  10. >>> class Foo:
  11. ... def __init__(self):
  12. ... self.count = 0
  13. ... @cached_property
  14. ... async def bar(self):
  15. ... await asyncio.sleep(1)
  16. ... self.count += 1
  17. ... return self.count
  18. ...
  19. >>> f = Foo()
  20. >>> await f.bar
  21. 1
  22. >>> await f.bar
  23. Traceback (most recent call last):
  24. File "/Users/dongwm/cpython/Lib/concurrent/futures/_base.py", line 439, in result
  25. return self.__get_result()
  26. File "/Users/dongwm/cpython/Lib/concurrent/futures/_base.py", line 388, in __get_result
  27. raise self._exception
  28. File "<console>", line 1, in <module>
  29. RuntimeError: cannot reuse already awaited coroutine
  30. pydanny/cached-property的异步支持实现的很巧妙,我把这部分逻辑抽出来:
  31. try:
  32. import asyncio
  33. except (ImportError, SyntaxError):
  34. asyncio = None
  35. class cached_property:
  36. def __get__(self, obj, cls):
  37. ...
  38. if asyncio and asyncio.iscoroutinefunction(self.func):
  39. return self._wrap_in_coroutine(obj)
  40. ...
  41. def _wrap_in_coroutine(self, obj):
  42. @asyncio.coroutine
  43. def wrapper():
  44. future = asyncio.ensure_future(self.func(obj))
  45. obj.__dict__[self.func.__name__] = future
  46. return future
  47. 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功能,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对w3xue网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号