[Effective Python] BW 26. functools.wrap을 사용해 함수 데코레이터를 정의하라

전민수·2023년 5월 25일
0

EffectivePython

목록 보기
6/10
post-thumbnail

데코레이터(Decorator)

파이썬에는 데코레이터라고 하는 특별한 구문을 제공합니다.

이 데코레이터라는 이름답게 함수를 꾸며주는 기능을 합니다.
자신이 감싸고 있는 함수가 호출되기 전과 후에 코드를 추가로 실행시킵니다.

함수의 의미를 강화하거나 디버깅하거나 함수를 등록하는 등의 기능을 수행할 수 있습니다.

Example

함수가 호출될 때마다 argument & return value를 출력하는 기능을 데코레이션을 활용하여 구현해보겠습니다.

def trace(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f'{func.__name__}({args!r}, {kwargs!r}) '
              f'-> {result!r}')
        return result
    return wrapper

trace 함수를 살펴보면 'func 함수를 매개변수로 받고, func 함수를 내부에서 실행하고, 그 결과를 출력하는 wrapper 함수'를 정의합니다. func 함수의 결과를 출력하는 기능을 추가한 wrapper 함수를 trace 함수 return으로 반환합니다.

@trace
def fibonacci(n):
    """Return n 번째 피보나치 수"""
    if n in (0, 1):
        return n
    return (fibonacci(n - 2) + fibonacci(n - 1))

fibonacci(4)

print(fibonacci)

데코레이터는 @ 기호를 사용하여 호출할 수 있습니다.

@trace
def fibonacci(n):

위 구문은

fibonachi = trace(fibonachi)

라고 해석할 수 있습니다.
즉, 데코레이터(trace 함수)가 반환한 결과를 원래 함수가 속해야하는 영역에 원래 함수와 같은 이름으로 등록하는 것과 같습니다.

따라서 wrapper는 func 함수에서 재귀 스택의 매 단계마다 함수의 인수와 반환값을 출력합니다.

fibonachi(4)

>>>
fibonacci((0,), {}) -> 0
fibonacci((1,), {}) -> 1
fibonacci((2,), {}) -> 1
fibonacci((1,), {}) -> 1
fibonacci((0,), {}) -> 0
fibonacci((1,), {}) -> 1
fibonacci((2,), {}) -> 1
fibonacci((3,), {}) -> 2
fibonacci((4,), {}) -> 3

부작용 : original function의 메타데이터 휘발

하지만 이 구문에는 부작용이 있습니다.
앞서 말했 듯, 데코레이터가 반환하는 함수는 wrapper 함수입니다. 즉 original function의 meta data가 보존되지 못합니다.

print(fibonacci)
>>>
<function trace.<locals>.wrapper at 0x00000228D112F280>

이런 동작은 디버거와 같이 인트로스펙션1을 하는 도구에서 문제가 됩니다.

1) 인트로스펙션(introspection) : 실행 시점에 프로그램이 어떻게 실행되는지 관찰하는 것

예를 들면, 데코레이트 된 fibonacci 함수에 help 내장 함수를 호출하면, 쓸모가 없어집니다. help 함수를 실행하면 fibonacci에 입력한 독스트링이 출력되어야 하지만, 그렇지 않습니다.

help(fibonacci)

>>>
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

이와 같은 맥락으로 원래 함수인 fibonacci의 meta data를 찾을 수 없어, 함수의 위치를 찾을 수 없기 때문에 객체 직렬화1도 깨집니다.

객체 직렬화 : 파이썬의 객체를 일련의 바이트들로 변환한 후 나중에 다시 파이썬 객체로 복원하게 할 수 있는데, 이렇게 파이썬 객체를 일련의 바이트들로 변환하는 것

해결책 : wraps 도우미 함수 사용

데코레이터를 활용하면서도 위의 부작용을 해결하기 위해서 wraps 도우미 함수를 쓸 수 있습니다.

functools 내장 모듈에 정의된 wraps 도우미 함수는 데코레이터 작성을 돕는 '데코레이터' 입니다.

wraps 데코레이터는 인수로 입력되는 함수의 메타데이터를 데코레이트하는 함수에 복사해서 적용해 줍니다.

from functools import wraps

def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f'{func.__name__}({args!r}, {kwargs!r}) '
              f'-> {result!r}')
        return result
    return wrapper


@trace
def fibonacci(n):
    """Return n 번째 피보나치 수"""
    if n in (0, 1):
        return n
    return (fibonacci(n - 2) + fibonacci(n - 1))

help(fibonacci)
>>>
Help on function fibonacci in module __main__:

fibonacci(n)
    Return n 번째 피보나치 수

이렇게 데코레이터를 사용하면서도 기존 함수의 어트리뷰트와 객체 정렬화까지 유지할 수 있게 됩니다.

profile
Learning Mate

0개의 댓글