-
[Python] Tip - functools.wraps로 함수 데코레이터를 정의언어/파이썬 & 장고 2017. 1. 6. 21:33
파이썬에는 함수에 적용할 수 있는 데코레이터라는 특별한 문법이 있습니다. 데코레이터는 감싸고 있는 함수를 호출하기 전이나 후에 추가로 코드를 실행하는 기능을 갖췄습니다. 이 기능으로 입력 인수와 반환 값을 접근하거나 수정할 수 있습니다. 이 기능은 시맨틱 강조, 디버깅, 함수 등록을 비롯해 여러 상황에 유용합니다.
예를 들어 함수를 호출할 때 인수와 반한 값을 출력하고 싶다고 가정합니다. 특히, 재귀 호출에서 함수 호출의 스택을 디버깅할 때 도움이 됩니다. 그럼 이런 데코레이터를 정의해보겠습니다.
def trace(func): def wrapper(*args, **kwargs): result = func(*args, **kwargs) print('%s(%r, %r) -> %r' % (func.__name__, args, kwargs, result)) return result return wrapper # @ 기호로 이 데코레이터를 함수에 적용 @trace def fibonacci(n): """n번째 피보나치 수를 반환""" if n in (0, 1): return n return (fibonacci(n - 2) + fibonacci(n - 1)) # @기호는 감싸고 있는 함수를 인수로 사용하여 해당 데코레이터를 호출한 후 반환 값을 같은 스코프에 있는 원래 이름에 할당하는 코드에 상응 fibonacci = trace(fibonacci) # 데코레이터를 호출하면 fibonacci 실행 전후에 wrapper 코드를 실행하여 재귀 스택의 각 단계마다 인수와 반환 값을 출력 fibonacci(3) # 결과 # fibonacci((1,), {}) -> 1 # wrapper((1,), {}) -> 1 # fibonacci((0,), {}) -> 0 # wrapper((0,), {}) -> 0 # fibonacci((1,), {}) -> 1 # wrapper((1,), {}) -> 1 # fibonacci((2,), {}) -> 1 # wrapper((2,), {}) -> 1 # fibonacci((3,), {}) -> 2 # wrapper((3,), {}) -> 2
이 코드는 잘 동작하지만 의도하지 않은 부작용을 일으킵니다. 즉, 데코레이터에서 반환한 값(앞에서 호출한 함수)의 이름이 fibonacci가 아닙니다.
print(fibonacci) # <function trace.<locals>.wrapper at 0x1006adc80>
원인은 어렵지 않게 찾을 수 있습니다. trace 함수는 그 안에 정의된 wrapper를 반환합니다. 이 wrapper 함수가 바로 데코레이터를 호출한 후 해당 호출을 담고 있는 모듈의 fibonacci라는 이름에 할당되는 값입니다. 이 동작은 디버거나 객체 직렬화 기능처럼 객체 내부를 조사하는 도구를 사용할 때 문제가 될 수 있습니다.
예를 들어 데코레이터를 적용한 fibonacci 함수에는 내장 함수 help가 쓸모없습니다.
help(fibonacci) # 결과 # Help on function wrapper in module __main__: # wrapper(*args, **kwargs)
해결책은 내장 모듈 functools의 wraps 헬퍼 함수를 사용하는 것입니다. wraps는 데코레이터를 작성하는 데 이용하는 데코레이터입니다. 이 데코레이터를 wrapper 함수에 적용하면 내부 함수에 있는 중요한 메타데이터가 모두 외부함수로 복사됩니다.
from functools import wraps def trace(func): @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) print('%s(%r, %r) -> %r' % (func.__name__, args, kwargs, result)) return result return wrapper # @ 기호로 이 데코레이터를 함수에 적용 @trace def fibonacci(n): """n번째 피보나치 수를 반환""" if n in (0, 1): return n return (fibonacci(n - 2) + fibonacci(n - 1)) fibonacci = trace(fibonacci) # 이제 help 함수를 실행하면 대상 함수에 데코레이터를 적용했더라도 원하는 결과를 얻을 수 있음 help(fibonacci) # 결과 # Help on function fibonacci in module __main__: # fibonacci(n) # n번째 피보나치 수를 반환
help를 호출한 예는 데코레이터가 어떤 식으로 미묘한 문제를 일으키는지 보여주는 사례 중 하나일 뿐입니다. 파이썬 함수에는 여러 표준 속성(예를 들면 __name__, __module__)이 있으며, 언어에서 함수들의 인터페이스를 유지하려면 이 속성들을 반드시 보호해야 합니다. wraps를 사용하면 항상 올바른 동작을 얻을 수 있습니다.
요약
데코레이터는 런타임에 한 함수로 다른 함수를 수정할 수 있게 해주는 파이썬 문법
데코레이터를 사용하면 디버거와 같이 객체 내부를 조사하는 도구가 이상하게 동작할 수도 있음
직접 데코레이터를 정의할 때 이런 문제를 피하려면 내장 모듈 functools의 wraps 데코레이터를 사용