Python 데코레이터 만드는 방법 - 오름캠프

프로필 사진mingke

Python Decorator

목차

파이썬 데코레이터

파이썬 데코레이터는 함수나 클래스를 감싸서 꾸며주는 기능을 합니다. 여기서 꾸민다는 것은 어떤 코드를 의미합니다. A라는 함수에 B라는 데코레이터를 감싸서 A코드가 실행될 때 B코드도 함께 실행되는 것이라고 생각하면 됩니다.

공통된 기능을 데코레이터로 만들어 중복을 줄여 재사용성을 높일 수 있습니다. 수업시간에 나왔던 예시에 로그인된 유저만 사용할 수 있는 기능에 로그인 검증 기능을 데코레이터로 만들어서 씌운것이 있었죠.

이해하기 어려운 개념은 아닌데 중요한 개념입니다. 실제로 저는 신입 면접 때 파이썬 데코레이터를 손으로 직접 구현해보라는 요청을 받기도 했습니다.

이번 포스팅에서 파이썬 데코레이터를 만드는 방법을 알아보도록 하겠습니다.

데코레이터 만들기

함수를 이용해서 만들기

파이썬의 함수는 일급객체입니다. 따라서 함수를 인자로 전달하거나, 함수에서 함수를 반환하는 것이 가능합니다. 이 특징을 데코레이터 만드는데 이용할 수 있습니다.

  • 데코레이터로 사용할 함수 시그니쳐를 먼저 선언합니다.
  • 데코레이터 함수 안에 Wrapper함수를 작성합니다.
  • Wrapper함수를 반환합니다.

코드로 보겠습니다.

import time
 
def time_trace(func):
    """함수의 실행시간을 기록하는 데코레이터"""
    def wrapper(*args, **kwargs):
        start_time = time.time()
        func(*args, **kwargs)
        end_time = time.time()
        print(end_time - start_time)
    return wrapper
 
 
@time_trace
def iterate_100000():
    for i in range(100000):
        i
    print("끝")
 
iterate_100000()
# 끝
# 0.0011379718780517578
  • 선언된 time_trace가 데코레이터로 사용됩니다.
  • 데코레이터의 매개변수 func는 time_trace가 꾸밀 함수를 가리킵니다. 꾸며질 함수를 인자로 받습니다.
  • *args,**kwargs가 필요한 이유는 꾸며질 함수에 파라미터가 있을 수 있기 때문에 그것까지 받아줘야 하기 때문입니다. wrapper 내부에서 func를 call하는데 그러려면 인자값이 필요하죠. 그러니 time_trace가 받아서 전달하는 것입니다.
  • wrapper함수를 time_trace함수 안에 선언했습니다. 보통 꾸미는 로직의 코드는 wrapper안에 작성됩니다.
  • time_trace는 wrapper를 반환합니다.
  • @ 를 이용해서 사용합니다.
  • time_trace(iterate_100000)() 이렇게 사용할 수도 있습니다.

클래스로 만들기

위 예시를 보면 함수를 입력받아 함수를 반환합니다. 클래스도 함수처럼 Callable하게 만들수 있기 때문에 데코레이터를 만들 수 있습니다.

import time
 
class TimeTrace:
    def __init__(self, func):
        self.func = func
 
    def __call__(self, *args, **kwargs):
        start_time = time.time()
        self.func(*args, **kwargs)
        end_time = time.time()
        print(end_time - start_time)
 
@TimeTrace
def iterate_100000():
    for i in range(100000):
        i
    print("끝")
 
iterate_100000()
  • 함수로 만들기 예시와 동일한 결과를 냅니다.

데코레이터를 만드는 방법을 요약하면 함수를 입력받아서 꾸미는 함수를 반환한다. 꾸미는 함수는 데코레이터 함수 내부에 선언된다. 이렇게 생각하면 됩니다.

클래스를 꾸미는 데코레이터

앞의 예시들을 조금 더 발전시켜보도록 합시다. 데코레이터가 입력으로 func를 받아서 실행했습니다. 함수가 아니라 class를 입력으로 받는다면 클래스를 꾸미는 데코레이터를 만들어볼 수도 있을 것 같습니다. 이 부분은 앞에 함수로 데코레이터 만들기가 이해가 되었다면 읽어보세요.

import time
import inspect
import functools
 
def time_trace(cls):
    # Scope 1
    def time_checker(method):
        # Scope 2
        @functools.wraps(method)
        def wrapper(*args, **kwargs):
            # Scope 3
            class_name = args[0].__class__.__name__
            start_time = time.time()
            # 메소드 CALL
            result = method(*args, **kwargs)
            end_time = time.time()
            print(f"{class_name}.{method.__name__}{(end_time - start_time) * 1000:.2f} ms 걸립니다.")
            return result
        return wrapper
 
    for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
        if not name.startswith("__"):
            setattr(cls, name, time_checker(method))
    return cls
 
@time_trace
class Temp:
    def iterate_100000(self):
        for i in range(100000):
            i
        print("끝")
 
obj = Temp()
obj.iterate_100000()
  • time_trace가 입력으로 cls를 받습니다. → 클래스를 꾸미겠다는 이야기죠
  • 클래스의 모든 메소드가 실행될 때 동작하는 데코레이터입니다. 따라서 내부에 time_checker라는 함수를 선언하고 입력으로 method를 받습니다.
  • time_checker안에 wrapper를 만들었고 그안에서 꾸밀 때 사용할 코드를 작성합니다. 함수로 만들 때와 동일합니다.
  • inspect.getmembers는 입력받은 클래스에서 inspect.isfunction, 함수(메소드)인 코드와 이름을 반환합니다. —> 로직은 __으로 시작하는 매직매소드는 제외하였습니다.
  • 함수 안에 함수가 계속 선언되기 때문에 Scope를 잘 구분해야합니다.

@functools.wraps

예제에 보면 @functools.wraps 라는 코드가 보입니다. 데코레이터를 만들때 자주 사용하는데요. 무엇인지 한 번 알아보겠습니다.

wraps는 데코레이터를 만들 때 wrapper함수에 적용하여 꾸며지는 함수의 메타데이터를 보존해주는 역할을 합니다. 코드로 한 번 알아보겠습니다.

# 위 함수 데코레이터 예제의 코드를 활용해보겠습니다.
# 코드 변경 없이 함수의 메타데이터 출력
print(iterate_100000.__doc__) # None
print(iterate_100000.__name__) # wrapper
print(iterate_100000.__module__) # __main__
print(iterate_100000.__annotations__) # {}
  • iterate_100000 함수의 메타데이터를 출력했는데 wrapper의 메타데이터가 출력되고 있습니다.

비교를 위해 iterate_100000 의 메타데이터를 time_trace 데코레이터 없이 출력해보겠습니다.

# doc과 annotations를 채웠습니다.
@time_trace
def iterate_100000():
    for i in range(100000):
        i
    print("끝")
 
def iterate_100000() -> None:
    """iterate_100000의 doc"""
    for i in range(100000):
        i
    print("끝")
 
print(iterate_100000.__doc__) # iterate_100000의 doc
print(iterate_100000.__name__) # iterate_100000
print(iterate_100000.__module__) # __main__
print(iterate_100000.__annotations__) # {'return': None}
  • 데코레이터를 지우니 메타데이터가 잘 출력되는 것을 볼 수 있습니다.
  • 데코레이터를 씌우면 함수의 메타데이터가 덮어씌워진다는 것을 알 수 있습니다.

이번에는 wrapper에 wraps 를 씌워보겠습니다.

import time
import functools
 
def time_trace(func):
    """함수의 실행시간을 기록하는 데코레이터"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        func(*args, **kwargs)
        end_time = time.time()
        print(end_time - start_time)
    return wrapper
 
 
@time_trace
def iterate_100000() -> None:
    """iterate_100000의 doc"""
    for i in range(100000):
        i
    print("끝")
 
 
print(iterate_100000.__doc__) # iterate_100000의 doc
print(iterate_100000.__name__) # iterate_100000
print(iterate_100000.__module__) # __main__
print(iterate_100000.__annotations__) # {'return': None}
  • iterate_100000 의 메타데이터가 유지되는 것을 확인할 수 있습니다.
  • 클래스로 데코레이터를 만든다면 functools.wraps를 사용하기가 어렵습니다. 데코레이터를 만들 때는 함수로 만드는 것을 권장합니다.
  • 이와 관련해서 신입 면접때 질문을 받은 적이 있습니다. functools.wraps 에 대해서 설명해보라는 질문이었습니다.

마무리

파이썬 데코레이터 만드는 방법에 대해서 수업시간에 배운 내용에 내용을 추가해서 알아봤습니다. 데코레이터는 많이 백엔드 실무에서도 자주 사용되기 때문에 잘 알아두는 것이 좋습니다.

Loading...