본문 바로가기
잡지식 저장고/Python

데코레이터란 : Python Decorator 예시집 - 2

by Slate_Knowledge 2023. 3. 5.
728x90

이전 포스팅 :

데코레이터란 : Python Decorator 예시집 - 1

 

데코레이터란 : Python Decorator 예시집 - 1

데코레이터? 파이썬에서 데코레이터란, 일반적으로 함수의 앞뒤에 미리 정의해둔 처리를 추가적으로 수행할 수 있게끔 치장하는 역할을 수행하는 컴포넌트이다. 아래와 같은 logger 함수를 데코

doodlrudco.tistory.com

이전 포스팅은, wrapper 함수를 좀 더 용이하고 쉽게 사용할 수 있도록 하는 파이썬의 데코레이터와 그때의 메타데이터 상속을 도와주는 @wraps 데코레이터에 대해 다뤘다면, 이후 포스팅은 유용하게 사용될 수 있는 데코레이터의 용례를 다룬다.

functools - @lru_cache

LRU는 기존 OS에서도 많이 다루는 캐시 관리 기법인데, Least Recently Used의 약자(참고링크)이다. 기본적으로 가장 오래전에 사용된 데이터가 현재 가장 쓸모없을 가능성이 높다는 가정하에 캐시에서 버려버리고 가장 최근까지 사용된 순으로 캐시에 저장하는 방식이다. 이러한 캐시는 동일한 결과를 여러번 query하는 경우 유용하게 사용될 수 있고, 캐시 크기가 충분하지 않은 경우 딥러닝 학습과 같이 한 에포크 내에서 동일한 입력이 없을 때 메모리만 낭비하게 되므로 잘 생각하고 사용해야 효과를 볼 수 있다.

직관적으로 이러한 cache의 이득을 가장 톡톡히 볼 수 있는 경우는 아래와 같다.

  • 한 입력에 대해 연산 시간이 매우 오래 걸리고
  • 특정 입력에 대해 자주 연산이 호출되며
  • 입력에 대해 출력이 정해져있는 경우(deterministic)

예를 들어서, 주어진 수 n에 대해서 팩토리얼을 계산한 다음 그 값을 리턴하는 함수를 아래와 같이 짰다고 가정하자.

import time
from functools import wraps, lru_cache

def time_tester(function, n=10):
    @wraps(function)
    def wrapper(*args, **kwargs):
        """tester documentation"""
        print(f"----- {function.__name__}: start -----")
        elapsed = 0
        for _ in range(n):
            st = time.time()
            output = function(*args, **kwargs)
            elapsed += time.time()-st
        print(f"----- {function.__name__}: end -----")
        print(f"Elapsed : {elapsed:0.5f}sec")
        return output
    return wrapper

@time_tester
def factorial(n):
    """factorial documentation"""
    answer = 1
    for k in range(1, n+1):
        answer *= k
    return answer

n이 20000이 넘어가면 아래의 결과와 같이(팩토리얼의 계산을 효율적으로 만들어서 빠르게 만들수도 있지만, 이 포스팅에서는 그 부분은 무시한다.) 꽤나 긴 (~162ms) 실행시간을 보이게 된다. (이때, 시간 재는건 time_tester 데코레이터를 이용해서 10번의 평균 수행시간을 잰다.)

Naive Factorial 수행시간

이제, 상기 예시에서 아래와 같이 factorial 함수를 바꿔보자.

@logger
@lru_cache(maxsize=None)
def factorial(n):
    """factorial documentation"""
    answer = 1
    for k in range(1, n+1):
        answer *= k
    return answer

그러면 아래와 같이 처음 호출 이후 급속도로 빠른 접근시간을 확인할 수 있다.

Cache Hit시 빨라지는 팩토리얼 연산시간. 10 usec 아래로 내려간다.

위와 같이 강력한 가속을 가져올수도 있는게 lru_cache 이지만, 입력에 대해 출력이 deterministic 하지 않은 경우. 즉 함수 내부 어딘가에 랜덤성이 개입하는 부분이 있다면 이 데코레이터의 사용이 원치 않는 결과를 초래할 수 있다.

import time
from functools import wraps, lru_cache

def logger(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        """wrapper documentation"""
        print(f"----- {function.__name__}: start -----")
        output = function(*args, **kwargs)
        print(f"----- {function.__name__}: end -----")
        return output
    return wrapper

import random
@logger
@lru_cache(maxsize=None)
def say_hello(n):
    """hello documentation"""
    print('hello')
    time.sleep(1)
    return n + random.random()

위와 같이 주어진 입력에 랜덤한 수를 더해 출력하도록 함수를 짰는데 lru_cache를 사용하면 어떻게 될까, 원래 사용자가 원하는 출력 형태는 아래와 같을 것이다.

그런데, 실제로 실행해보면 아래와 같은 결과가 나온다.

주어진 입력에 대해서 저장된 출력값을 그대로 내뱉기 때문에, 랜덤 구문이 씹혀버린다(덤으로 print 구문처럼 함수 내부에서 시스템 출력을 하도록 해뒀다면 이것도 씹힌다!). 따라서, lru_cache 데코레이터는 함수의 용례에 맞춰서 적절히 사용해야 하겠다.

728x90
반응형

댓글