파이썬에는 데코레이터라고 하는 특별한 구문을 제공합니다.
이 데코레이터라는 이름답게 함수를 꾸며주는 기능을 합니다.
자신이 감싸고 있는 함수가 호출되기 전과 후에 코드를 추가로 실행시킵니다.
함수의 의미를 강화하거나 디버깅하거나 함수를 등록하는 등의 기능을 수행할 수 있습니다.
함수가 호출될 때마다 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
하지만 이 구문에는 부작용이 있습니다.
앞서 말했 듯, 데코레이터가 반환하는 함수는 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 도우미 함수를 쓸 수 있습니다.
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 번째 피보나치 수
이렇게 데코레이터를 사용하면서도 기존 함수의 어트리뷰트와 객체 정렬화까지 유지할 수 있게 됩니다.