Decorator

연어는결국강으로·2024년 4월 24일
0

Python

목록 보기
1/1

1. 공부하게 된 이유

본의 아니게 openai api 강의를 들으면서 파이썬을 공부하게 되었다. 본래 생각은 자바에서도 쓸 수 있는 restapi가 있을거라고 생각하며 강의를 샀는데 그게 아니었다.... 파이썬으로 된 라이브러리를 사용하는 형식이었다.

어쨋든 오늘은 강의를 들으면서 sqlite를 쓰는 부분에서 데코레이터 패턴을 사용해서 이 부분을 기록하려고 글을 작성하게 되었다.

2. 데코레이터 패턴 예시 코드 및 구조

먼저 데코레이터를 정의한 함수를 보면 아래와 같다.

def memoize_to_sqlite(filename: str = "cache.db"):

	# 함수 내부 로직
    
    def memoize(func):

		def wrapped(*args):
			# 함수 내부 로직
            return result

        return wrapped

    return memoize

이 코드는 Python에서 데코레이터 패턴의 기본 구조를 나타낸다. 데코레이터는 일반적으로 함수를 수정하지 않고 추가 기능을 제공하고자 할 때 사용된다.

데코레이터의 구조를 자세히 살펴보자.

  1. 데코레이터 팩토리 (memoize_to_sqlite):

    • memoize_to_sqlite는 데코레이터 팩토리 함수이다. 이 함수는 데코레이터 함수인 memoize를 반환한다. 데코레이터 팩토리는 필요한 설정(예: 데이터베이스 파일 이름)을 데코레이터에 전달할 수 있게 해준다.
  2. 데코레이터 (memoize):

    • memoize는 실제 데코레이터 함수로, 데코레이션 대상이 되는 함수 func를 인자로 받는다. 이 함수는 내부적으로 또 다른 함수 wrapped를 정의하고 반환한다.
  3. 래핑된 함수 (wrapped):

    • wrapped 함수는 데코레이터가 적용된 원본 함수의 로직을 감싸는 래퍼(wrapper) 역할을 한다. wrapped는 원본 함수 func가 호출될 때 실행되며, *args를 사용해 원본 함수에 전달된 모든 위치 인수를 받는다.
    • 이 함수 내에서 원본 함수의 실행 전후로 추가적인 작업을 수행할 수 있으며, 그 결과를 반환한다.

위 데코레이터의 사용 예는 다음과 같다:

@memoize_to_sqlite(filename="embeddings.db")
@retry(
    wait=wait_random_exponential(multiplier=1, max=30),
    stop=stop_after_attempt(3),
    retry=retry_if_exception_type(APIConnectionError) | retry_if_exception_type(APIError) | retry_if_exception_type(RateLimitError),
)
def get_embedding(text: str) -> List[float]:
	# ... 기타 코드들 ...
    return embeddings

여기서 @memoize_to_sqlite(filename="embeddings.db")some_expensive_function 함수를 memoize_to_sqlite로 데코레이트한다. 이렇게 하면 some_expensive_function을 호출할 때마다 실제 함수 호출 대신 wrapped 함수가 호출되어 캐싱 로직 등 추가적인 기능이 수행된다.

데코레이터의 강력함은 기존 코드를 수정하지 않고도 함수의 기능을 확장할 수 있다는 데 있다. 그 결과, 코드의 재사용성과 유지보수성이 향상된다.

3. 내부 동작 원리

다시 돌아가서 위의 memoize_to_sqlite를 호출하면 어떻게 내부의 memoize가 호출되고 wrapped가 호출되는지 알아보자.

1) *args는 무엇인가

먼저 wrapped함수에서 쓰인 *args가 무엇인지 알아봐야 한다. *args는 파이썬에서 위치 인수를 가변적으로 받을 수 있게 하는 문법입니다. 함수에 얼마나 많은 인수가 전달될지 모를 때, 이들을 튜플로 묶어서 처리할 수 있다. wrapped 함수 내에서 *argsfunc 함수로 전달되는 모든 인수들을 받아들인다. 예를 들어, some_expensive_function(x, y) 함수가 있다면, 이 함수에 xy라는 인수가 전달될 때 wrapped 함수의 args(x, y)라는 튜플로 이들을 받는다.

2) func를 받고 wrapped를 반환하는 과정

memoize 함수는 데코레이터의 핵심 부분으로, 원본 함수 func를 인자로 받는다. 이 함수는 내부적으로 wrapped 함수를 정의하고 이 wrapped 함수를 반환한다. return wrapped라는 구문을 통해, memoize 데코레이터가 적용된 함수가 호출될 때 실제로 실행되는 것은 wrapped 함수이다.

3) wrapped 함수의 호출 과정

데코레이터를 적용한 함수를 호출하면, 다음과 같은 일이 일어난다:
1. 원본 함수 funcmemoize에 전달한다.
2. memoizewrapped를 정의하고 반환한다.
3. 이제 원본 함수 대신 wrapped 함수가 func의 자리를 차지하게 된다.
4. 데코레이터가 적용된 함수를 호출하면 사실상 wrapped 함수가 호출되며, 이 함수 내부에서 원본 함수 func가 필요할 때만 호출된다.

4) 코드 실행 예

@memoize_to_sqlite("cache.db")
def some_expensive_function(x):
    return x * x

# 함수 호출
some_expensive_function(4)

위 예에서 some_expensive_function(4)를 호출하면 실제로는 wrapped(4)가 실행된다. wrappedsome_expensive_function의 인수 4를 받아 처리하고, 이 때의 로직에 따라 결과를 캐시하거나 캐시된 결과를 반환한다.

이런 식으로 데코레이터는 기존 함수의 기능을 변경하지 않으면서 추가적인 기능을 실행할 수 있는 강력한 수단을 제공한다. wrapped는 실질적으로 원본 함수의 프록시 역할을 하며, 데코레이터는 이를 활용해 다양한 부가 기능을 구현할 수 있다.

0개의 댓글