ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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 데코레이터를 사용


    댓글