Python

[Python] Wrapper에 대해서(@wraps)

nebulayoon 2024. 12. 21. 11:37

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를 사용 할 필요가 없겠으나, 디버깅을 쉽게 하려면, 그냥 쓰는 것을 추천할 것 같다.