В этой статье хочется рассмотреть декоратор cached_property. Почему он есть и в стандартной библиотеке и в Django. Чем они отличаются и когда какой лучше использовать

Проблема

Допустим у нас есть класс с property, которое вычислять довольно долго, но мы им пользуемся часто и не хочется вычислять его несколько раз.

Пример класса:

import dataclasses
import hashlib


@dataclasses.dataclass
class User:
    first_name: str
    last_name: str

    @property
    def signature(self) -> bytes:
        return hashlib.sha512((self.first_name + self.last_name).encode()).digest()

Наивная реализация

Первая идея, которая может прийти в голову это сделать приватный атрибут и в нём хранить закешированный результат

import dataclasses
from typing import Optional
import hashlib


@dataclasses.dataclass
class User:
    first_name: str
    last_name: str
    _signature: Optional[bytes] = dataclasses.field(init=False, repr=False, compare=False, hash=False, default=None)

    @property
    def signature(self) -> bytes:
        if self._signature is None:
            self._signature = hashlib.sha512((self.first_name + self.last_name).encode()).digest()
        return self._signature

И получится довольно хорошее решение. В нём есть один недостаток - нам приходится добавлять приватный метод, если у нас таких property много у класса, то у нас будет очень много атрибутов, что не очень хорошо.

Решение из модуля functools

Тогда стоит обратить внимание на cached_property в модуле functools.

Ниже представлен пример с использованием functools.cached_property

import dataclasses
import functools
from typing import Optional
import hashlib


@dataclasses.dataclass
class User:
    first_name: str
    last_name: str
  
    @functools.cached_property
    def signature(self) -> bytes:
        return hashlib.sha512((self.first_name + self.last_name).encode()).digest()

Этот декоратор сделан так, что если ты вызовешь метод signature параллельно несколько раз из разных потоков, то функция вызовется один раз(наивное решение не давало таких гарантий).

То есть код ниже вызовет функцию hashlib.sha512 только один раз

user = User(first_name='Andrei', last_name='Berenda')

tasks = [
    threading.Thread(target=lambda: user.signature)
    for i in range(10)
]
for task in tasks:
    task.start()

for task in tasks:
    task.join()

Но нужно разобраться каким образом это сделано.
Если посмотреть на реализацию, то можем увидеть, что cached_property использует локи и лок берется на весь класс, а не на объект класса. То есть мы не сможем начать выполнять параллельно несколько сигнатур для разных объектов класса.

Проблемы с functools.cached_property

Если мы в метод signature поместим запрос в базу или поход по http (то есть любую операцию, которая не блокирует GIL), мы всё равно будем ждать завершения метода, перед тем, как начать выполнять эту же функцию на другом объекте

import dataclasses
import datetime
import functools
import time
from typing import Optional
import hashlib
import threading

@dataclasses.dataclass
class User:
    first_name: str
    last_name: str
    _signature: Optional[bytes] = dataclasses.field(init=False, repr=False, compare=False, hash=False)

    @functools.cached_property
    def signature(self) -> bytes:
        time.sleep(1)
        return b'signed'

      
tasks = [
    threading.Thread(target=lambda: User(first_name='Andrei', last_name='Berenda').signature)
    for i in range(10)
]
now = datetime.datetime.now()
for task in tasks:
    task.start()

for task in tasks:
    task.join()
print('finished', datetime.datetime.now() - now)

Код выше будет выполняться больше 10 секунд (для упрощения я использовал time.sleep(1), но можно было использовать поход в базу).

Хотя если мы будем использовать первоначальное решение, то оно будет занимать немного больше секунды (что в 10 раз быстрее).

import dataclasses
import datetime
import time
from typing import Optional
import threading

@dataclasses.dataclass
class User:
    first_name: str
    last_name: str
    _signature: Optional[bytes] = dataclasses.field(init=False, repr=False, compare=False, hash=False, default=None)

    @property
    def signature(self) -> bytes:
        if self._signature is None:
            time.sleep(1)
            self._signature = b'signed'
        return self._signature


tasks = [
    threading.Thread(target=lambda: User(first_name='Andrei', last_name='Berenda').signature)
    for i in range(10)
]
now = datetime.datetime.now()
for task in tasks:
    task.start()

for task in tasks:
    task.join()
print('finished', datetime.datetime.now() - now)

Решение от Django

Эту особенность заметили в Django и написали свой декоратор cached_property, который не гарантирует что метод будет вызван только один раз, но работает намного быстрее в многопоточном приложении (каким и является приложение с использованием Django).

import dataclasses
import datetime
import threading
import time

from django.utils.functional import cached_property


@dataclasses.dataclass
class User:
    first_name: str
    last_name: str

    @cached_property
    def signature(self) -> bytes:
        time.sleep(1)
        return b'signed'


tasks = [
    threading.Thread(target=lambda: User(first_name='Andrei', last_name='Berenda').signature)
    for i in range(10)
]
now = datetime.datetime.now()
for task in tasks:
    task.start()

for task in tasks:
    task.join()
print('finished', datetime.datetime.now() - now)

Код выше будет работать примерно так же как и наше решение(будет отрабатывать за 1 секунду).

Подведение итогов

Если у нас есть функция, которую вы хотите кешировать и её вызывать несколько раз для одного и того же объекта крайне нежелательно, то в таком случае можно использовать functools.cached_property (или можно попробовать написать свой декоратор, который будет брать локи на уровне объекта, а не на уровне класса), а во всех остальных случаях я бы использовать cached_property из Django (если вы не используете django, то можно просто скопировать код, там не очень много кода).

Комментарии (11)


  1. Helltraitor
    00.00.0000 00:00
    +3

    Если мы в метод signature поместим запрос в базу или поход по http

    Первое: если ..., то нельзя будет утверждать, что метод чистый, а значит кэширование может повлечь нежелательные последствия.

    Второе: если ..., то свойство (проперти) будет работать неожиданно медленно. Поскольку свойство должно быть максимально легким, если оно все таки используется (представьте себе это проперти, которое будет стучатся условную секунду в базу данных). Иначе говоря, при частом использовании могут возникнуть просадки производительности (если свойство ходит по http, например)

    Это тот случай, когда один вопрос на известном сайте лучше всей статьи. Прошу к ознакомлению: https://stackoverflow.com/questions/68593165/what-is-the-difference-between-cached-property-in-django-vs-pythons-functools


    1. funca
      00.00.0000 00:00

      В питоне есть давняя традиция: проталкивать свежесочиненный код в стандартную библиотеку, а потом всем сообществом годами страдать и исправлять в нем баги.

      Про @functools.cached_property есть отдельная дискуссия: https://discuss.python.org/t/finding-a-path-forward-for-functools-cached-property/23757/41. tldr; уже ни кто не сомневается, что лок на классе это баг. Но как починить - коллективый разум пока не придумал.


  1. Denis-Alexeev
    00.00.0000 00:00
    -1

    from functools import cache
    from time import time
    
    class User:
        @property
        @cache
        def signature(self) -> bytes:
            time.sleep(1)
            return b'signed'
    

    Потокобезопасно и нет необходимости копировать код или писать свой велосипед.

    И чтоб 2 раза не вставать, вопрос не по теме.
    Уже не в первый раз встречаю людей, которые экземпляр (instance) класса называют объектом класса. Скажите, почему? В каких книгах так пишут?
    В python всё есть объект. И класс - это объект, и экземпляр - это объект.
    Фраза "объект класса" указывает на "питонячью" реализацию сущности класса. Но никак не его экземпляра (инстанса).


    1. mayorovp
      00.00.0000 00:00
      +4

      Да это общая оговорка для ООП в принципе. А берётся она вот откуда:


      1. Во-первых, слово "экземпляр" язык сломаешь произносить, да и забыть его легко, а "инстанс" глупая калька которую произносить стыдно. Произнести же или написать "объект класса" можно вообще не задумываясь.


      2. Во-вторых, "объект класса" — это "объект, имеющий какое-то отношение к классу". А отношение между объектами и классами в ООП есть ровно одно, если оставаться в рамках стандартного ООП без динамических "расширений". Так что "объект класса", хоть и не является термином, всё ещё синоним "экземпляру".



      Фраза "объект класса" указывает на "питонячью" реализацию сущности класса.

      А вот и нет, "питонячья" реализация сущности класса (class object) на русский язык правильно переводится как "объект-класс". Это же не объект, который имеет какие-то отношение к классу. а объект который буквально является классом.


      Впрочем, произнося "объект-класс" язык сломать ещё проще чем произнося "экземпляр", так что я понимаю тех кто говорит "объект класса" имея в виду "class object".


      1. Denis-Alexeev
        00.00.0000 00:00
        -1

        слово "экземпляр" язык сломаешь произносить

        Ваше личное мнение. Я его легко произношу. Мои коллеги тоже проблем не испытывают

        да и забыть его легко

        Без комментириев

        "инстанс" глупая калька которую произносить стыдно

        Снова ваше личное мнение. Мне не стыдно.

        "объект класса" — это "объект, имеющий какое-то отношение к классу".
        "питонячья" реализация сущности класса (class object) на русский язык правильно переводится как "объект-класс"

        Был бы вам очень благодарен за ссылки на более или менее уважаемые источники, где такой перевод относительно объектов и классов является общепризнанным.

        "питонячья" реализация сущности класса (class object)

        я понимаю тех кто говорит "объект класса" имея в виду "class object"

        объект класса", хоть и не является термином, всё ещё синоним "экземпляру".

        А тут вы сами себе противоречите. В одном предложении у вас "объект класса" - это синоним "экземпляра". В другом под "объект класса" вы понимаете "class object". А "class object" у вас это сам класс.


        1. mayorovp
          00.00.0000 00:00

          Был бы вам очень благодарен за ссылки на более или менее уважаемые источники, где такой перевод относительно объектов и классов является общепризнанным.

          Языковой логики вам недостаточно?


          А тут вы сами себе противоречите.

          Никакого противоречия. "Объект класса" — это не термин, а описательный оборот, который может обозначать разные вещи в зависимости от контекста. У неграмотных авторов — ещё и в одном предложении.


          1. Denis-Alexeev
            00.00.0000 00:00
            -1

            Языковой логики вам недостаточно?

            Вы утверждаете, что (class object) на русский язык правильно переводится как "объект-класс", при этом ссылаетесь на языковую логику, а не на имеющиеся переводы (любые)? На мой взгляд, это некорректный перевод терминологии. К примеру, reverso фразу "class object" переводит как "объект класса" или "объект Class". Гугл как "объект класса". Ни в какой литературе по python я никогда не встречал определения экземпляра, как "объект класса". Но справедливости ради я предпочитаю англоязычную литературу.

            Языковой логики вам недостаточно?

            Мне, как человеку, у которого русский - родной, а английский используется ежедневно в работе, данная логика видится некорректной.

            Никакого противоречия. "Объект класса" — это не термин, а описательный оборот, который может обозначать разные вещи в зависимости от контекста.

            Если мы всё ещё говорим о python, то нет. В python есть такие определения как Class object, Instance object, Method object и т.п.


            1. mayorovp
              00.00.0000 00:00

              Вы утверждаете, что (class object) на русский язык правильно переводится как "объект-класс", при этом ссылаетесь на языковую логику, а не на имеющиеся переводы (любые)?

              Да, именно так. Потому что имеющиеся переводы — это либо машинные, либо от переводчиков которые смотрели на имеющиеся переводы вместо того чтобы вникать в смысл переводимого текста.


              Кстати, там ниже привели словосочетание "an object of a class". Как вы его переводить на русский язык будете?


              Мне, как человеку, у которого русский — родной, а английский используется ежедневно в работе, данная логика видится некорректной.

              Что некорректного вы видите в утверждении "class object — это объект, который является классом"?


              Если мы всё ещё говорим о python, то нет. В python есть такие определения как Class object, Instance object, Method object и т.п.

              Ну да, объект-класс, объект-экземпляр, объект-метод. А вы их как называете? Неужели "объект экземпляра" и "объект метода"?


              1. Denis-Alexeev
                00.00.0000 00:00
                -2

                Да, именно так. Потому что имеющиеся переводы — это либо машинные, либо от переводчиков которые смотрели на имеющиеся переводы вместо того чтобы вникать в смысл переводимого текста.

                Ясно. Значит интернет не прав. И я не прав. И все с кем довелось работать - тоже не правы. А вы правы. Ок. Но ссылок не будет? Будем пришивать к делу личные ощущения?

                Кстати, там ниже привели словосочетание "an object of a class". Как вы его переводить на русский язык будете?

                объект класса

                Что некорректного вы видите в утверждении "class object — это объект, который является классом"?

                некорректного я вижу во фразах:

                "объект класса" — это "объект, имеющий какое-то отношение к классу".

                "питонячья" реализация сущности класса (class object) на русский язык правильно переводится как "объект-класс"

                А вы их как называете? Неужели "объект экземпляра" и "объект метода"?

                да, именно так. Но аргумент был не к тому, как я это называю, а к вашему высказыванию

                "Объект класса" — это не термин, а описательный оборот,

                В документации python "Class object" - это вполне себе термин, однозначно описывающий объект класса. При этом у экземпляра класса есть отдельный термин - Instance object. Нигде в документации python экземпляр класса не называется class object.


                1. mayorovp
                  00.00.0000 00:00
                  +1

                  Кстати, там ниже привели словосочетание «an object of a class». Как вы его переводить на русский язык будете?
                  объект класса
                  То есть вы считаете нормальным переводить «an object of a class» и «class object» одинаково, хотя они означают совсем разные вещи, но неправ я? Отлично.


                  Если вы согласны с тем, что class object — это объект, который является классом, тогда объясните мне, каким таким образом словосочетание «объект класса» в русском языке может означать «объект, который является классом».

                  Какие такие грамматические правила заставили слово «класс» оказаться в родительном падеже.


    1. funca
      00.00.0000 00:00
      +1

      которые экземпляр (instance) класса называют объектом класса. Скажите, почему? В каких книгах так пишут?

      Это калька с английского an object of a class. Теоретики иногда ещё говорят an element of a class. В большинстве случаев an instance of a class означает то же самое. В чисто практическом смысле, всегда найдутся более важные задачи, чем искать разницу между ними https://stackoverflow.com/q/2885385.