В этой статье хочется рассмотреть декоратор 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)
Denis-Alexeev
00.00.0000 00:00-1from functools import cache from time import time class User: @property @cache def signature(self) -> bytes: time.sleep(1) return b'signed'
Потокобезопасно и нет необходимости копировать код или писать свой велосипед.
И чтоб 2 раза не вставать, вопрос не по теме.
Уже не в первый раз встречаю людей, которые экземпляр (instance) класса называют объектом класса. Скажите, почему? В каких книгах так пишут?
В python всё есть объект. И класс - это объект, и экземпляр - это объект.
Фраза "объект класса" указывает на "питонячью" реализацию сущности класса. Но никак не его экземпляра (инстанса).mayorovp
00.00.0000 00:00+4Да это общая оговорка для ООП в принципе. А берётся она вот откуда:
-
Во-первых, слово "экземпляр" язык сломаешь произносить, да и забыть его легко, а "инстанс" глупая калька которую произносить стыдно. Произнести же или написать "объект класса" можно вообще не задумываясь.
-
Во-вторых, "объект класса" — это "объект, имеющий какое-то отношение к классу". А отношение между объектами и классами в ООП есть ровно одно, если оставаться в рамках стандартного ООП без динамических "расширений". Так что "объект класса", хоть и не является термином, всё ещё синоним "экземпляру".
Фраза "объект класса" указывает на "питонячью" реализацию сущности класса.
А вот и нет, "питонячья" реализация сущности класса (class object) на русский язык правильно переводится как "объект-класс". Это же не объект, который имеет какие-то отношение к классу. а объект который буквально является классом.
Впрочем, произнося "объект-класс" язык сломать ещё проще чем произнося "экземпляр", так что я понимаю тех кто говорит "объект класса" имея в виду "class object".
Denis-Alexeev
00.00.0000 00:00-1слово "экземпляр" язык сломаешь произносить
Ваше личное мнение. Я его легко произношу. Мои коллеги тоже проблем не испытывают
да и забыть его легко
Без комментириев
"инстанс" глупая калька которую произносить стыдно
Снова ваше личное мнение. Мне не стыдно.
"объект класса" — это "объект, имеющий какое-то отношение к классу".
"питонячья" реализация сущности класса (class object) на русский язык правильно переводится как "объект-класс"Был бы вам очень благодарен за ссылки на более или менее уважаемые источники, где такой перевод относительно объектов и классов является общепризнанным.
"питонячья" реализация сущности класса (class object)
я понимаю тех кто говорит "объект класса" имея в виду "class object"
объект класса", хоть и не является термином, всё ещё синоним "экземпляру".
А тут вы сами себе противоречите. В одном предложении у вас "объект класса" - это синоним "экземпляра". В другом под "объект класса" вы понимаете "class object". А "class object" у вас это сам класс.
mayorovp
00.00.0000 00:00Был бы вам очень благодарен за ссылки на более или менее уважаемые источники, где такой перевод относительно объектов и классов является общепризнанным.
Языковой логики вам недостаточно?
А тут вы сами себе противоречите.
Никакого противоречия. "Объект класса" — это не термин, а описательный оборот, который может обозначать разные вещи в зависимости от контекста. У неграмотных авторов — ещё и в одном предложении.
Denis-Alexeev
00.00.0000 00:00-1Языковой логики вам недостаточно?
Вы утверждаете, что (class object) на русский язык правильно переводится как "объект-класс", при этом ссылаетесь на языковую логику, а не на имеющиеся переводы (любые)? На мой взгляд, это некорректный перевод терминологии. К примеру, reverso фразу "class object" переводит как "объект класса" или "объект Class". Гугл как "объект класса". Ни в какой литературе по python я никогда не встречал определения экземпляра, как "объект класса". Но справедливости ради я предпочитаю англоязычную литературу.
Языковой логики вам недостаточно?
Мне, как человеку, у которого русский - родной, а английский используется ежедневно в работе, данная логика видится некорректной.
Никакого противоречия. "Объект класса" — это не термин, а описательный оборот, который может обозначать разные вещи в зависимости от контекста.
Если мы всё ещё говорим о python, то нет. В python есть такие определения как Class object, Instance object, Method object и т.п.
mayorovp
00.00.0000 00:00Вы утверждаете, что (class object) на русский язык правильно переводится как "объект-класс", при этом ссылаетесь на языковую логику, а не на имеющиеся переводы (любые)?
Да, именно так. Потому что имеющиеся переводы — это либо машинные, либо от переводчиков которые смотрели на имеющиеся переводы вместо того чтобы вникать в смысл переводимого текста.
Кстати, там ниже привели словосочетание "an object of a class". Как вы его переводить на русский язык будете?
Мне, как человеку, у которого русский — родной, а английский используется ежедневно в работе, данная логика видится некорректной.
Что некорректного вы видите в утверждении "class object — это объект, который является классом"?
Если мы всё ещё говорим о python, то нет. В python есть такие определения как Class object, Instance object, Method object и т.п.
Ну да, объект-класс, объект-экземпляр, объект-метод. А вы их как называете? Неужели "объект экземпляра" и "объект метода"?
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.
mayorovp
00.00.0000 00:00+1Кстати, там ниже привели словосочетание «an object of a class». Как вы его переводить на русский язык будете?
объект класса
Если вы согласны с тем, что class object — это объект, который является классом, тогда объясните мне, каким таким образом словосочетание «объект класса» в русском языке может означать «объект, который является классом».
Какие такие грамматические правила заставили слово «класс» оказаться в родительном падеже.
-
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.
Helltraitor
Первое: если ..., то нельзя будет утверждать, что метод чистый, а значит кэширование может повлечь нежелательные последствия.
Второе: если ..., то свойство (проперти) будет работать неожиданно медленно. Поскольку свойство должно быть максимально легким, если оно все таки используется (представьте себе это проперти, которое будет стучатся условную секунду в базу данных). Иначе говоря, при частом использовании могут возникнуть просадки производительности (если свойство ходит по http, например)
Это тот случай, когда один вопрос на известном сайте лучше всей статьи. Прошу к ознакомлению: https://stackoverflow.com/questions/68593165/what-is-the-difference-between-cached-property-in-django-vs-pythons-functools
funca
В питоне есть давняя традиция: проталкивать свежесочиненный код в стандартную библиотеку, а потом всем сообществом годами страдать и исправлять в нем баги.
Про @functools.cached_property есть отдельная дискуссия: https://discuss.python.org/t/finding-a-path-forward-for-functools-cached-property/23757/41. tldr; уже ни кто не сомневается, что лок на классе это баг. Но как починить - коллективый разум пока не придумал.