﻿---
title: Decorator @cached_property
date: 2025-06-18
excerpt: Web框架源码中常见的装饰器，缓存属性值，避免重复计算
tags: [Python, Decorator, Django]
cover: https://assets.vluv.space/cover/Lang/Python/cached_property.webp
updated: 2026-05-08 22:10:51
---

## Intro

Defination: Decorator that converts a method with a single self argument into a property cached on the instance.

```python
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__` 字典中，后续访问时直接返回缓存值，不再重新计算

> [!NOTE] Two related Dunder methods
>
> [set_name — Python 3.13.5 documentation](https://docs.python.org/3/reference/datamodel.html#object.__set_name__)
> [get — Python 3.13.5 documentation](https://docs.python.org/3/reference/datamodel.html#object.__get__)

### Source Code

```python
# 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)
```

```mermaid
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、表单数据等计算成本较高的属性 |

```python
# 并非同一个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
```
