외부효과(side effect)가 없고 특정한 인풋에 대해 고정된 아웃풋이 나오는 함수를 순수함수(pure function)이라고 합니다.

이런 함수는 특정 인풋에 대한 아웃풋을 캐싱해두면, 같은 인풋이 들어왔을 때 다시 계산할 필요 없이, 캐싱된 아웃풋을 리턴하면 됩니다.

이미 정의된 순수 함수를 손쉽게 캐싱을 할 수 있게 해주는 데코레이터들이 functools 모듈에 있습니다.

cache, cached_property, lru_cache가 바로 그것 들입니다.

데코레이터이기 때문에 함수 정의 바로 위에 @cache, @cached_property, @lru_cache 를 써두는 것만으로 함수가 결과값들을 캐싱할 수 있게 해줍니다.

1. cache

파이썬 3.9 부터 사용 가능

특정 인풋에 대한 함수의 결과값을 저장합니다.(memoize)

from functools import cache

@cache
def my_function(n):
    print(n)

def main():
    my_function(2) # 2
    my_function(3) # 3
    my_function(4) # 4
    my_function(3) # 출력되지 않음
    my_function(4) # 출력되지 않음

if __name__ == '__main__':
    main()

my_function 함수를 cache로 데코레이팅 한 결과, main 함수에서의 my_function 호출이 각 인풋에 대해 캐싱되므로, 매개변수 3, 4에 대한 호출이 다시 일어났을 때 내부 코드가 실행되지 않습니다.

2. cached_property

파이썬 3.8부터 사용 가능

property() 데코레이터에 캐싱 기능이 추가된 것입니다.

property() 처럼 클래스의 메소드를 프로퍼티화 합니다.

기존의 property()는 프로퍼티 메소드가 실행될 때마다 매번 계산해야 하지만,

cached_property()는 한번만 계산하고, 결과를 저장한 후 다시 실행될 때 저장된 결과를 리턴합니다.

기존의 property()에 대한 프로퍼티 메소드는 write 연산이 불가능하지만,

cached_property()는 write 연산이 가능합니다.

from functools import cached_property

class Student:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    @cached_property
    def name(self):
        print('get name')
        return self._name
    @property
    def age(self):
        print('get age')
        return self._age

def main():
    st = Student("stlee", 32)
    st.name # get name, 단 한번만 메소드 실행
    st.name # 출력 없음
    st.age # get age
    st.age # get age, 프로퍼티에 접근할 때마다 메소드 실행
    st.name = "Seungtae" # 새로운 값 할당 가능
    # st.age = 33, AttributeError: can't set attribute 'age'
    print(st.name) # Seungtae


if __name__ == '__main__':
    main()

cached_property는 연산한 값을 객체의 __dict__에 저장하기 때문에 __slots__로 프로퍼티를 지정하는 경우는 작동하지 않습니다.

from functools import cached_property

class Student:
    def __init__(self, name, age):
        self._name = name
        self._age = age
    @cached_property
    def name(self):
        return self._name
    @property
    def age(self):
        return self._age

def main():
    st = Student("stlee", 32)
    
    
    for k, v in st.__dict__.items():
        print('key', k, 'value', v)
    # 프로퍼티 호출 전
    # key _name value stlee
    # key _age value 32
    
    
    st.name
    for k, v in st.__dict__.items():
        print('key', k, 'value', v)
    # 프로퍼티 새로운 값 할당 전
    # key _name value stlee
    # key _age value 32
    # key name value stlee, name 키에 stlee값이 생겼다.
       
    st.name = "Seungtae"
    for k, v in st.__dict__.items():
        print('key', k, 'value', v)
    # 프로퍼티 새로운 값 할당 후
    # key _name value stlee
    # key _age value 32
    # key name value Seungtae, name 키에 새로운 값 Seungtae가 할당됨.


if __name__ == '__main__':
    main()

3. lru_cache

lru_cache는 위의 cache와 비슷한 기능을 하지만, maxsize와 typed 파라미터를 받습니다.

lru_cache는 값을 dictionary에 저장하기 때문에, 키로 이용되는 파라미터가 hashable 해야합니다.

  • maxsize : 캐시의 최대 크기, 기본값 128, None으로 지정되면 크기에 제한이 없다.(cache와 같음)
  • typed : 파라미터가 같은 값으로 hash 되더라도 타입별로 구분할것인지 여부, 기본값 False

lru는 Least Recently Used의 약자로 가장  최근에 사용된 파라미터를 최대 maxsize까지 캐싱한다는 의미입니다.

lru_cache는 함수에 3개의 메소드를 추가합니다.

  • cache_parameters : lru_cache를 호출할 때 주어진 파라미터 maxsize, typed값
  • cache_info : 현재까지의 캐시 hits, misses, maxsize, cursize를 named tuple로 리턴
  • cache_clear : 캐시 비우기
from functools import lru_cache

@lru_cache
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

def main():
    fib(10)
    print(fib.cache_parameters()) # {'maxsize': 128, 'typed': False}, 기본값
    print(fib.cache_info()) # CacheInfo(hits=8, misses=11, maxsize=128, currsize=11)
    fib.cache_clear() # 캐시 비우기
    print(fib.cache_info()) # CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)

if __name__ == '__main__':
    main()

typed가 기본적으로 False이기 때문에, 파라미터의 타입이 다르더라도 같은 값으로 hash된다면 같은 키로 간주됩니다.

from functools import lru_cache
from decimal import Decimal
from fractions import Fraction

@lru_cache
def showType(obj):
    return type(obj)

def main():
    t = showType(Decimal(10))
    print(t) # <class 'decimal.Decimal'>
    t = showType(Fraction(10))
    print(t) # <class 'decimal.Decimal'>

if __name__ == '__main__':
    main()

Decimal(10)과 Fraction(10)의 해쉬값이 같기 때문에, 같은 키로 인식되어 Fraction(10)을 넣었을 때도 Decimal(10)의 값이 나옵니다.

typed를 True로 놓게 되면, 키를 구분할때 타입도 보기 때문에, 따로 저장 됩니다.

from functools import lru_cache
from decimal import Decimal
from fractions import Fraction

@lru_cache(typed=True)
def showType(obj):
    return type(obj)

def main():
    t = showType(Decimal(10))
    print(t) # <class 'decimal.Decimal'>
    t = showType(Fraction(10))
    print(t) # <class 'fractions.Fraction'>

if __name__ == '__main__':
    main()

+ Recent posts