网站建设资讯

NEWS

网站建设资讯

Python3.8中怎么实现一个functools.cached_property功能

这篇文章将为大家详细讲解有关Python 3.8中怎么实现一个functools.cached_property功能,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。

我们一直强调成都做网站、网站设计对于企业的重要性,如果您也觉得重要,那么就需要我们慎重对待,选择一个安全靠谱的网站建设公司,企业网站我们建议是要么不做,要么就做好,让网站能真正成为企业发展过程中的有力推手。专业网站建设公司不一定是大公司,创新互联作为专业的网络公司选择我们就是放心。

bottle.cached_property

Bottle是我最早接触的Web框架,也是我第一次阅读的开源项目源码。最早知道 cached_property 就是通过这个项目,如果你是一个Web开发,我不建议你用这个框架,但是源码量少,值得一读。

werkzeug.utils.cached_property

Werkzeug是Flask的依赖,是应用 cached_property 最成功的一个项目。

pip._vendor.distlib.util.cached_property

PIP是Python官方包管理工具。

kombu.utils.objects.cached_property

Kombu是Celery的依赖。

django.utils.functional.cached_property

Django是知名Web框架,你肯定听过。

甚至有专门的一个包: pydanny/cached-property。

如果你犯过他们的代码其实大同小异,在我的观点里面这种轮子是完全没有必要的。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 "", line 1, in

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: .wrapper at 0x10a26f0c0>

In : await f.bar # 第一次获得f.bar的值,会sleep 1秒然后返回结果

Out: 1

In : f.__dict__['bar'] # 这样就把Task对象缓存到了f.__dict__里面了,Task状态是finished

Out: :4> result=1>

In : f.bar # f.bar已经是一个task了

Out: :4> result=1>

In : await f.bar # 相当于 await task

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

关于Python 3.8中怎么实现一个functools.cached_property功能就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。


本文名称:Python3.8中怎么实现一个functools.cached_property功能
分享链接:http://cdweb.net/article/jschoo.html