@cached property: примеры (PYTHON)

Использование декоратора cached_property для эффективного кэширования
Раздел: Декораторы, Кэширование
@cached_property(func: callable): cached_property

Описание декоратора @cached_property

Декоратор @cached_property, доступный в модуле functools начиная с Python 3.8, преобразует метод класса в свойство, значение которого вычисляется только один раз за время жизни экземпляра. После первого обращения результат кэшируется и повторно используется при последующих вызовах, что повышает производительность для ресурсоемких вычислений.

Этот декоратор применяется к методам, которые не принимают аргументов, кроме self. Он не имеет собственных параметров для настройки. Возвращаемым значением является экземпляр cached_property, который ведет себя как дескриптор. При первом обращении к атрибуту выполняется декорированный метод, а его результат сохраняется в одноименном поле экземпляра. Последующие чтения возвращают кэшированное значение напрямую, без вызова метода.

Основное использование - оптимизация вычислений, результат которых стабилен для конкретного объекта. Декоратор не предназначен для свойств, которые должны пересчитываться при изменении внутреннего состояния.

Базовые примеры применения

Пример с вычислением площади круга:

from functools import cached_property

class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    @cached_property
    def area(self):
        print('Вычисление площади')
        return 3.14159 * self.radius ** 2

c = Circle(5)
print(c.area)  # Первый вызов
print(c.area)  # Второй вызов
Вычисление площади
78.53975
78.53975

Пример с использованием в условии:

class DataProcessor:
    def __init__(self, data):
        self.data = data
    
    @cached_property
    def is_valid(self):
        print('Проверка валидности')
        return len(self.data) > 0

proc = DataProcessor([1, 2, 3])
if proc.is_valid and proc.is_valid:
    print('Данные корректны')
Проверка валидности
Данные корректны

Альтернативные подходы в Python

Обычное свойство @property: Вычисляется каждый раз при обращении. Подходит для динамических данных, но неэффективно для тяжелых операций.

Декоратор @functools.lru_cache: Кэширует результаты вызовов функции с учетом аргументов. Может использоваться для методов, но требует аккуратности при работе с изменяемыми объектами.

Ручное кэширование в __init__: Вычисление значения в конструкторе и сохранение в атрибут. Просто, но не всегда удобно, если вычисление зависит от сложной логики.

Библиотечные решения: Например, django.utils.functional.cached_property в Django, который работает схожим образом.

Распространенные ошибки

Использование с методами, принимающими аргументы: Декоратор работает только с методами, у которых есть единственный параметр self.

from functools import cached_property

class Example:
    @cached_property
    def method(self, x):  # Ошибка
        return x * 2

e = Example()
print(e.method(5))
TypeError: method() missing 1 required positional argument: 'x'

Изменение зависимых данных без инвалидации кэша: Если атрибуты, используемые в вычислении, изменяются, кэшированное значение становится неактуальным.

class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    @cached_property
    def area(self):
        return 3.14159 * self.radius ** 2

c = Circle(5)
print(c.area)  # 78.53975
c.radius = 10
print(c.area)  # Остается старым значение
78.53975
78.53975

Попытка присвоения значения: По умолчанию @cached_property не поддерживает сеттер, но его можно добавить.

История изменений

Декоратор @cached_property был добавлен в Python 3.8. В более ранних версиях использовался аналогичный декоратор в Django или реализовывался вручную. Начиная с Python 3.8, он является частью стандартной библиотеки, что обеспечивает переносимость. В версии Python 3.12 были оптимизированы внутренние механизмы работы декоратора, но публичный API остался неизменным.

Расширенные сценарии использования

Пример с инвалидацией кэша через удаление атрибута:

Пример python
from functools import cached_property

class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    @cached_property
    def area(self):
        print('Вычисление площади')
        return 3.14159 * self.radius ** 2

c = Circle(5)
print(c.area)
del c.area  # Сброс кэша
print(c.area)
Вычисление площади
78.53975
Вычисление площади
78.53975

Использование с наследованием классов:

Пример python
class Shape:
    @cached_property
    def description(self):
        return "Базовая фигура"

class Square(Shape):
    def __init__(self, side):
        self.side = side
    
    @cached_property
    def area(self):
        return self.side ** 2

s = Square(4)
print(s.description)
print(s.area)
Базовая фигура
16

Кэширование результата работы с файлом или сетью:

Пример python
import json
from functools import cached_property

class ConfigLoader:
    def __init__(self, filepath):
        self.filepath = filepath
    
    @cached_property
    def data(self):
        print('Загрузка конфигурации')
        with open(self.filepath, 'r') as f:
            return json.load(f)

loader = ConfigLoader('config.json')
print(loader.data['app_name'])
print(loader.data['version'])
Загрузка конфигурации
MyApp
1.0

Реализации в других языках

JavaScript (геттер): Используется ключевое слово get, но без автоматического кэширования.

class Circle {
    constructor(radius) {
        this.radius = radius;
        this._area = null;
    }
    get area() {
        if (!this._area) {
            console.log('Вычисление площади');
            this._area = Math.PI * this.radius ** 2;
        }
        return this._area;
    }
}
let c = new Circle(5);
console.log(c.area);
console.log(c.area);
Вычисление площади
78.53981633974483
78.53981633974483

Java (Lombok): Аннотация @Getter(lazy=true) генерирует код с ленивой инициализацией.

import lombok.Getter;

public class Circle {
    private final double radius;
    
    @Getter(lazy=true)
    private final double area = calculateArea();
    
    private double calculateArea() {
        System.out.println("Вычисление площади");
        return Math.PI * radius * radius;
    }
}

C# (свойство с ленивой загрузкой): Используется Lazy для отложенной инициализации.

using System;

public class Circle
{
    private readonly double radius;
    private readonly Lazy area;
    
    public Circle(double radius)
    {
        this.radius = radius;
        area = new Lazy(() => {
            Console.WriteLine("Вычисление площади");
            return Math.PI * radius * radius;
        });
    }
    
    public double Area => area.Value;
}

питон @cached_property function comments

En
@cached property Decorator that converts a method into a cached property