Decorator @Cached_property
Intro
Defination: Decorator that converts a method with a single self argument into a property cached on the instance.
1
2
3
4
5
6
7
8
9
10
11
from functools import cached_property
import json5 as json
class ConfigLoader:
@cached_property
def config(self):
with open('settings.json', 'r') as f:
return json.load(f)
config = ConfigLoader().config
print(config)
Principle
源码逻辑比较简单,当首次访问被装饰的属性时,执行原始方法,将计算结果存储在instance的 __dict__
字典中,后续访问时直接返回缓存值,不再重新计算
Two related Dunder methods
Source Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# python3.13 functools.py
################################################################################
### cached_property() - property result cached as instance attribute
################################################################################
_NOT_FOUND = object()
class cached_property:
def __init__(self, func):
self.func = func
self.attrname = None
self.__doc__ = func.__doc__
self.__module__ = func.__module__
def __set_name__(self, owner, name):
# Automatically called at the time the owning class owner is created.
if self.attrname is None:
self.attrname = name
elif name != self.attrname:
raise TypeError(
"Cannot assign the same cached_property to two different names "
f"({self.attrname!r} and {name!r})."
)
def __get__(self, instance, owner=None):
# Automatically called when the property is accessed.
if instance is None: # True when accessed through the class, e.g. MyClass.attr
return self
if self.attrname is None:
raise TypeError(
"Cannot use cached_property instance without calling __set_name__ on it.")
try:
cache = instance.__dict__
except AttributeError: # not all objects have __dict__ (e.g. class defines slots)
msg = (
f"No '__dict__' attribute on {type(instance).__name__!r} "
f"instance to cache {self.attrname!r} property."
)
raise TypeError(msg) from None
val = cache.get(self.attrname, _NOT_FOUND)
if val is _NOT_FOUND:
val = self.func(instance)
try:
cache[self.attrname] = val
except TypeError:
msg = (
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
f"does not support item assignment for caching {self.attrname!r} property."
)
raise TypeError(msg) from None
return val
__class_getitem__ = classmethod(GenericAlias)
View Mermaid diagram code
sequenceDiagram
participant User as 代码
participant Property as cached_property
participant Dict as instance.__dict__
Note over User,Dict: 首次访问属性
User->>Property: 访问属性 (obj.attr)
Property->>Dict: 检查是否有缓存值
Dict-->>Property: 返回 _NOT_FOUND (无缓存)
Property->>Property: 调用原始方法
Property->>Dict: 存储结果
Property-->>User: 返回结果
Note over User,Dict: 后续访问属性
User->>Dict: 访问属性 (obj.attr)
Dict-->>User: 直接返回缓存值
Note right of Dict: Python 查找属性时优先检查 __dict__
Scenarios
访问频繁、计算耗时,通常为只读属性
Danger
Note: 如果对数据进行写入缓存并不会自动失效,需要手动del/set缓存
Examples
Case | Description |
---|---|
Cache ORM related fields | 将数据库查询的结果中存到实例中 |
Cache HTTP Request Object | 解析 JSON、表单数据等计算成本较高的属性 |
1
2
3
4
5
6
7
8
9
10
11
# 并非同一个module,但思路是一致的
# Demo Ref: https://medium.com/@esatyilmaz/introduction-c1306df1a84c
from django.utils.functional import cached_property
class Book(models.Model):
title = models.CharField(max_length=50)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
@cached_property
def author_full_name(self):
return self.author.full_name
Decorator @Cached_property