cached_property는 장고에서 제공하는 클래스로, 이를 이용해 특정 메소드로 리턴 되는 속성 값을 여러 번 호출해야 하는 경우(property 안에서 호출하는 함수의 비용이 큰 연산 작업의 경우) 결과값을 캐싱하여, 반복적인 연산이나 네트워크 I/O로 발생하는 딜레이를 줄여 프로그램의 성능을 향상시킬 수 있다.
cached_property는 decorator로 사용하며, 처음 호출된 property 함수 결과값을 캐싱해 둔 뒤 그 이후에는 캐싱된 결과를 리턴한다. 장고에서는 캐싱할 때 캐싱하고자 하는 메소드를 cached_property의 클래스로 등록해 dict에 저장한다. 그러면 우리는 해당 메소드를 호출할 때 저장된 메소드(캐싱된 메소드)에 캐싱된 값을 리턴하게 되어 같은 연산을 여러 번 하여 생기는 비용을 덜 수 있다.
비싼 연산작업이 필요하고, 여러번 호출되는 값은 @cached_property으로 캐싱해두자!
ex) DB / ElasticSearch / API 통신 등..
cached_property는 아래와 같이 생겼다.
class cached_property:
"""
Decorator that converts a method with a single self argument into a
property cached on the instance.
-> 단일 자체 인수가 있는 메서드를 인스턴스에 캐시된 속성으로 변환하는 데코레이터
A cached property can be made out of an existing method:
(e.g. ``url = cached_property(get_absolute_url)``).
The optional ``name`` argument is obsolete as of Python 3.6 and will be
deprecated in Django 4.0 (#30127).
"""
name = None
@staticmethod
def func(instance):
raise TypeError(
'Cannot use cached_property instance without calling '
'__set_name__() on it.'
)
def __init__(self, func, name=None):
self.real_func = func
self.__doc__ = getattr(func, '__doc__')
def __set_name__(self, owner, name):
if self.name is None:
self.name = name
self.func = self.real_func
elif name != self.name:
raise TypeError(
"Cannot assign the same cached_property to two different names "
"(%r and %r)." % (self.name, name)
)
def __get__(self, instance, cls=None):
"""
Call the function and put the return value in instance.__dict__ so that
subsequent attribute access on the instance returns the cached value
instead of calling cached_property.__get__().
"""
if instance is None:
return self
res = instance.__dict__[self.name] = self.func(instance)
return res
아래의 예시를 생각해 보자.
class TestClass(object):
def __init__(self):
self.called = 0
@cached_property
def called_count(self):
print("난 두 번 호출되진 않는다!")
self.called += 1
return self.called
아래와 같이 실행될 것이다.
t = TestClass()
print(t.called) # 0
print(t.called_count) # "난 두 번 호출되진 않는다!"\n 1
print(t.called_count) # 1
print(t.called_count) # 1
확인해 보면 처음 호출시에만 called + 1 연산을 수행하고 그 뒤부터는 캐싱된 self.called만 리턴한다.
from django.utils.functional import cached_property
from django.utils.timezone import datetime
from foods.utils import MenuAPI
class WhatToEat:
@cached_property
def menu(self):
target_date = datetime.today()
menu = MenuAPI.call(target_date=target_date)
return menu
@property
def menu_according_to_weather(self):
# 여기서 menu는 WhatToEat 인스턴스에 캐싱되어 self.menu로 호출할 수 있다.
self.menu['weather']
@property
def menu_according_to_temperature(self):
self.menu['temperature']
what_to_eat = WhatToEat()
print(f'날씨에 따른 메뉴 추천: {what_to_eat.menu_according_to_weather}')
print(f'온도에 따른 메뉴 추천: {what_to_eat.menu_according_to_temperature}')
여기서 만약 menu property를 캐싱해 두지 않았다면 MenuAPI는 2번 호출됐을 것이다. 이를 방지 하기 위해 @cached_property 데코레이터를 사용하면 캐시된 menu를 반환하기 때문에 MenuAPI는 처음 한 번만 호출된다. 즉, 처음 menu가 호출될 때 menu 메소드는 cached_property 클래스에 등록 되는 것이다.
다시 말해, 처음 menu_according_to_weather를 호출할 때 cached_property의 __get__
메소드를 호출하고 WhatToEat 인스턴스의 __dict__
의 key로 menu
를, 메소드를 실행했을 때 리턴되는 값을value
로 담는다. 그러면 그 다음 menu
를 호출할 때는 __get__
을 거치지 않고 딕셔너리에 저장된 value 값을 반환한다.
이전 파이썬 버전에서는 캐싱할 때 메소드 뿐만 아니라 docstring과 name까지 등록해 주었지만 파이썬 버전 3.6 이후에서는 사라졌으며 따라서 django 4.0부터는 TBD(To be deprecated)이다.
참고
1) 장고 docs
https://docs.djangoproject.com/en/4.0/ref/utils/
https://docs.djangoproject.com/en/4.0/_modules/django/utils/functional/#cached_property
2) 블로그