Caching과 setrecursionlimit() 함수

민성철·2023년 3월 4일
0

Python_study

목록 보기
4/4

Caching 이란?

일반적으로 캐싱(caching) 은 실행하는데 오래 걸리는 연산을 미리 계산해 놓고 저장하여, 재사용하는 기법을 의미합니다.
여기 까지는 이전에 메모이제이션(memoization)과 동일하게 보일텐데 그 이유는 캐싱의 범주에 속하기 때문입니다.
캐싱은 이 외에도 접근하는데 시간이 오래 걸리는 데이터를 접근 속도가 빠른 저장소에 사본을 저장해두고 재사용 하는 기법을 의미하기도 합니다.
(이 외에도 웹브라우저나 서버, 하드웨어에서도 각 용도별 캐싱이 있습니다.)

예시로 친구들과 국내여행을 다녔을 때, 방문 횟수는 제외하고 방문한 곳만 추려본다고 가정하겠습니다.

  1. 우선 국내여행을 다녀온 곳 부터 모아봤습니다.
def get_travel(place: str):
    print(f'우리가 여행갔던 곳은 "{place}"입니다.')


def fetch_place(place: str):
    return get_travel(place)


if __name__ == "__main__":
    from random import choice

    for i in range(10):
        fetch_place(place=choice(["Sokcho", "Gangneung", "Jeonju"]))

output

우리가 여행갔던 곳은 "Jeonju"입니다.
우리가 여행갔던 곳은 "Sokcho"입니다.
우리가 여행갔던 곳은 "Sokcho"입니다.
우리가 여행갔던 곳은 "Gangneung"입니다.
우리가 여행갔던 곳은 "Sokcho"입니다.
우리가 여행갔던 곳은 "Sokcho"입니다.
우리가 여행갔던 곳은 "Jeonju"입니다.
우리가 여행갔던 곳은 "Jeonju"입니다.
우리가 여행갔던 곳은 "Gangneung"입니다.
우리가 여행갔던 곳은 "Gangneung"입니다.

  1. 다음 으로 방문 횟수를 제외하고 방문한 곳만 추려볼텐데, 여기서 메모이제이션(memoization)을 활용하겠습니다.
    여기서 이전과 동일하게 리스트(List)를 활용하여 구현해보겠습니다.
def get_travel(place: str):
    print(f'우리가 여행갔던 곳은 "{place}"입니다.')


cache = []		# 리스트 선언


def fetch_place(place: str):
    if place not in cache:		# 조건으로 기존에 값이 없을경우만 해당
        cache.append(place)
    elif place in cache:		# 값이 있다면 함수를 종료
        return 0
    return get_travel(place)


if __name__ == "__main__":
    from random import choice

    for i in range(10):
        fetch_place(place=choice(["Sokcho", "Gangneung", "Jeonju"]))

output

우리가 여행갔던 곳은 "Sokcho"입니다.
우리가 여행갔던 곳은 "Jeonju"입니다.
우리가 여행갔던 곳은 "Gangneung"입니다.

잘 추려진것을 확인할 수 있습니다.


  1. 하지만 저희는 파이썬 내장된 모듈로 caching을 활용해보겠습니다.
    @cache 데코레이터를 사용하고자 하는 함수 위에 선언을 하게되면, 함수에 인자값을 키(key)로 그리고 반환값을 (value)로 메모이제이션이 적용됩니다.
    다만, @cache 데코레이터는 파이썬 3.9버전 이상만 사용 가능하기 때문에 유의해야 합니다.
from functools import cache		# 내장 모듈인 functools


def get_travel(place: str):
    print(f'우리가 여행갔던 곳은 "{place}"입니다.')


@cache		# 기존 코드를 @cache 데코레이터로 대체
def fetch_place(place: str):
    return get_travel(place)


if __name__ == "__main__":
    from random import choice

    for i in range(10):
        fetch_place(place=choice(["Sokcho", "Gangneung", "Jeonju"]))

이 외에도 "최근에 사용된 데이터일수록 앞으로도 사용될 가능성이 높다"라는 가설을 바탕으로 가장 오랫동안 사용되지 않은 데이터를 우선적으로 캐시에서 삭제하는 LRU(Least Recently Used), 이와 정반대의 접근 방법을 택하는 MRU(Most Recently Used), 그리고 사용 빈도를 고려한 LFU(Least Frequently Used) 등 그 밖에도 다양한 캐싱(caching) 전략이 있습니다.

  1. 그리고 @cache 데코레이터를 기반으로 .cache_into() 함수를 통해 정보를 알 수 있습니다.
    3~4  output
우리가 여행갔던 곳은 "Sokcho"입니다.
우리가 여행갔던 곳은 "Gangneung"입니다.
우리가 여행갔던 곳은 "Jeonju"입니다.
CacheInfo(hits=7, misses=3, maxsize=None, currsize=3)

hits는 캐시에 저장된 함수의 결과를 사용한 횟수, misses는 캐시에 일치하는 경우가 없어서(혹은, 조건에 따라 삭제되어서) 함수가 실행된 횟수, maxsize는 캐시의 최대크기 (매개변수에 전달된 인자값으로 @cache의 defoult값은 None으로 캐싱 전략마다 다른 defoult값을 가졌으며, 당연히 사용자 정의가 가능합니다.), currsize는 현재의 캐시저장용량 입니다.


setrecursionlimit() 함수

이전에 메모이제이션을 활용한 피보나치 수열을 Top-Down과 Bottom-Up을 예시로 코드를 작성했었습니다.
Bottom-Up은 큰 수도 무리없이 돌아가는 반면, Top-Down은 callstack이 초과되어 "Python 개체를 호출하는 동안 최대 재귀 깊이를 초과했다."는 error message를 봤었습니다.

때문에 해결하기 위해선 다른 방법을 사용하거나, 깊이를 조절해주면 됩니다.
파이썬의 기본 재귀 깊이 제한은 1000으로 매우 얕은 편에 속합니다.
따라서 재귀로 문제를 풀 경우 왠만하면 제한이 걸리게 되기 때문에 필수적으로 사용 된다고 봐도 무방합니다.

import sys
from functools import lru_cache

sys.setrecursionlimit(int(1e5)) # python 최대 깊이 지정

fib_arr = [0, 1]

@lru_cache(maxsize=3) # None과 동일한 hits, missing 수
def fib_recur_dp(number: int) -> int:
	if number < len(fib_arr):
		return fib_arr[number]
	else:
		fib = fib_recur_dp(number-1) + fib_recur_dp(number-2)
		fib_arr.append(fib)
		return fib


print(fib_recur_dp(3000))
print(fib_recur_dp.cache_info())
->
727 µs ± 127 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
CacheInfo(hits=11108, misses=3001, maxsize=3, currsize=3)

# When the parameter of lru_cache is None 
584 µs ± 177 µs per loop 
(.., currsize=3001)

여기서 유의해야할 점은 PyPy에서는 sys.getrecursionlimit() 함수로 재귀의 깊이를 임의설정 할 수 없습니다.

출처 - https://www.daleseo.com/python-cache/

profile
ENTJ-A

0개의 댓글