Django decorator | @cached_property

Jihun Kim·2021년 12월 31일
1

Django | decorator

목록 보기
1/4
post-thumbnail

@cached property

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

예시1

아래의 예시를 생각해 보자.

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만 리턴한다.

예시2

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) 블로그

https://sjquant.tistory.com/33

https://americanopeople.tistory.com/317

profile
쿄쿄

0개의 댓글