[Python] Wrapper에 대해서(@wraps)
Outline
데코레이터를 만들다가, @wraps는 내부적으로 어떻게 동작할까 궁금해서 글을 작성해본다.
Contents
기본적으로 @wraps는 기존 함수의 meta data를 살리기 위해서 사용한다.
그런데, wrapper 안에서 meta data를 호출 할 때, 인자로 받은 func의 meta data를 호출하면, @wraps와 wrapper.__name__을 호출하지 않고, 기존 함수의 meta data를 사용할 수 있다.
import functools
import time
def time_profiling(func):
# @functools.wraps(func)
def wrapper(*args, **kwargs):
"""this is wrapper"""
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__}: {end - start}")
print(f"{func.__doc__}")
return result
return wrapper
@time_profiling
def test(max: int):
""" "this is test function."""
return [i for i in range(1, max)]
test(100_100)
위의 코드를 실행시키면, func.__name__을 출력 시켰기 때문에, 데코레이터를 사용한 기존 함수인 "test" 가 출력 되는 것을 알 수 있다.
엥? 그러면 뭐하러 @wraps를 사용할까?
일반적으로 PyPI처럼 python package를 만드는게 아니라면, meta data를 쓸일이 없어서, 차이를 못 느낄 수도 있지만, 데코레이터 외부에서 meta data를 호출하면 문제가 된다.
import functools
import time
def time_profiling(func):
# @functools.wraps(func)
def wrapper(*args, **kwargs):
"""this is wrapper"""
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__}: {end - start}")
print(f"{func.__doc__}")
return result
return wrapper
@time_profiling
def test(max: int):
""" "this is test function."""
return [i for i in range(1, max)]
test(100_100)
print(test.__name__)
print(test.__doc__)
기존 코드에서 test에 대해서 __name__, __doc__을 출력하는 코드를 추가하였다.
분명, test 함수의 __name__, __doc__을 호출 했는데, test의 meta data가 아닌, wrapper 의 meta data가 출력되었다. 즉, wrapper의 meta data로 덮혔는데, 이를 해결하기 위해서 사용하는 것이 @functools.wraps 이다.
test_profiling을 데코레이터로 사용했음에도, wrapper가 표시되는 것이 아니라, test 그대로의 meta data가 출력 되는 것을 알 수 있다.
그리고, 번외로 wrapper안에서 func.__name__과 같이, 기존 함수에 대해서 meta 데이터를 호출 하지 않고, wrapper.__name__으로 사용하는 이유는, 아마 명시적으로 가독성을 높이기 위해서 암묵적으로 사용하는 것이 아닌 가 하는 생각이 든다.
Real contents(?)
이제야 본론에 도달했다. 그래서, @functools.wraps 데코레이터를 사용하면, 어떻게 기존 함수의 meta data를 살리는 걸까?
위 코드를 보면, wrapped에 test 함수가 들어가게 되고, update_wrapper의 wrapper 인자에는, 우리가 만든 wrapper가 들어가게된다.
놀랍도록, 정말 간단하게 동작하는데, 미리 정의된, __module__, __name__, __qualname__, __doc__, __annotations__, __dict__를 wrapper에 덮어 씌우거나, 업데이트 한다.
그리고, __wrapped__에 test함수를 넣어 준다.
정말 별거 없이, getattr, setattr로만 동작하는데, functools 패키지는 meta data를 다시 작업해주는 걸로 해결했다.
궁금하니까, 실제로 print(wrapper.__wrapped__)를 실행시켜보면,
위와 같이, test함수가 출력되는 것을 알 수 있다.
End
wraps를 왜 써야하는지 갑자기 궁금해져서, 분석해봤는데, 문제를 정말 쉽게 해결하고 있어서 좀 놀랐다. 반대로, meta 정보를 살릴 필요가 없다면, 굳이 @wraps를 사용 할 필요가 없겠으나, 디버깅을 쉽게 하려면, 그냥 쓰는 것을 추천할 것 같다.