파이썬을 사용하다보면 함수 또는 클래스 위에 @로 시작하는 녀석들을 볼 수 있다. 이러한 데코레이터를 사용하면 함수에 부적인 동작을 추가하거나 작동을 바꿀 수 있다. 잘 사용한다면 코드가 훨씬 깔끔해지기 때문에 어느정도 Python이 익숙해진 개발자라면 꼭 사용하는걸 권장한다.

decorator 만들기

def hello():
    print('hello 함수 시작')
    print('hello')
    print('hello 함수 끝')
 
def world():
    print('world 함수 시작')
    print('world')
    print('world 함수 끝')
 
hello()
world()

hello 함수 시작
hello
hello 함수 끝
world 함수 시작
world
world 함수 끝

위와 같은 간단한 코드가 있다고 해보자. 위 코드를 보면 hello()world()의 기능이 중복되는 것을 알 수 있다. 이런 중복을 처리하기 위해 아래와 같이 함수화하여 코드를 정리할 수 있다.

def trace(func):                    
    print(func.__name__, '함수 시작')     # __name__으로 함수 이름 출력
    func()                              
    print(func.__name__, '함수 끝')

def hello():
    print('hello')
 
def world():
    print('world')
 
trace(hello)
trace(world)

하지만 이렇게 사용하면 직접 trace() 함수를 사용하여 hello()함수를 호출해야만 내가 원하는 동작이 이루어진다는 단점이 있다. 이럴 때 더 깔끔하고 사용하기 쉽게하는 기능이 decorator다. 위 코드를 데코레이터 코드로 바꿔주면 아래와 같다.

def trace(func):                             # 호출할 함수를 매개변수로 받음
    def wrapper():
        print(func.__name__, '함수 시작')    # __name__으로 함수 이름 출력
        func()                               # 매개변수로 받은 함수를 호출
        print(func.__name__, '함수 끝')
    return wrapper                           # wrapper 함수 반환
 
@trace    # @데코레이터
def hello():
    print('hello')
 
@trace    # @데코레이터
def world():
    print('world')
 
hello()    # 함수를 그대로 호출
world()    # 함수를 그대로 호출

데코레이터를 사용하려면 wrapper()라는 함수가 필요하다. 이 함수로 감싸주고, 이를 return해주는 구조로 이루어져있다. 이후 데코레이터를 사용하고 싶은 함수 위에 @와 데코레이터 함수를 적어주면 사용할 수 있다. 이제 우리가 처음부터 원했던 모습으로 hello()world()함수의 코드를 정리하였다.

매개변수와 반환값 처리

def trace(func):          # 호출할 함수를 매개변수로 받음
    def wrapper(a, b):    # 호출할 함수 add(a, b)의 매개변수와 똑같이 지정
        r = func(a, b)    # func에 매개변수 a, b를 넣어서 호출하고 반환값을 변수에 저장
        print('{0}(a={1}, b={2}) -> {3}'.format(func.__name__, a, b, r))  # 매개변수와 반환값 출력
        return r          # func의 반환값을 반환
    return wrapper        # wrapper 함수 반환
 
@trace              # @데코레이터
def add(a, b):      # 매개변수는 두 개
    return a + b    # 매개변수 두 개를 더해서 반환
 
print(add(10, 20))
add(a=10, b=20) -> 30
30

만약 함수의 매개변수와 반환값을 데코레이터에서 처리하고 싶다면 위와 같이 작성하면 된다. 매개변수wrapper()에 데코레이터를 사용할 함수(여기서는 add())의 매개변수 형식과 동일하게 넣어주어 wrapper() 함수내에서 처리할 수 있다. 반환값wrapper()의 return을 통해 처리하면 된다.

그렇다면 매개변수가 있는 데코레이터는 어떻게 만들어야 할까?

def is_multiple(x):              # 데코레이터가 사용할 매개변수를 지정
    def real_decorator(func):    # 호출할 함수를 매개변수로 받음
        def wrapper(a, b):       # 호출할 함수의 매개변수와 똑같이 지정
            r = func(a, b)       # func를 호출하고 반환값을 변수에 저장
            if r % x == 0:       # func의 반환값이 x의 배수인지 확인
                print('{0}의 반환값은 {1}의 배수입니다.'.format(func.__name__, x))
            else:
                print('{0}의 반환값은 {1}의 배수가 아닙니다.'.format(func.__name__, x))
            return r             # func의 반환값을 반환
        return wrapper           # wrapper 함수 반환
    return real_decorator        # real_decorator 함수 반환
 
@is_multiple(3)     # @데코레이터(인수)
def add(a, b):
    return a + b
 
print(add(10, 20))
print(add(2, 5))
add의 반환값은 3의 배수입니다.
30
add의 반환값은 3의 배수가 아닙니다.
7

데코레이터 함수를 살펴보자. 최상위에 있는 is_multiple()을 통해 데코레이터가 입력받을 매개변수를 지정해준다. 그 밑의 real_decorator()를 통해 호출할 함수를 매개변수로 받는다. 즉, wrapper()를 한번 더 감싸준다고 생각하면 된다. 이러한 구조를 통해 데코레이터에 값을 직접 입력할 수 있다.

class로 decorator 만들기

from dataclasses import dataclass

@dataclass
class IsMultiple:
    x:int

    def __call__(self, func):
        def wrapper(a, b):
            r = func(a, b)
            if r%self.x == 0:
                print('{0}의 반환값은 {1}의 배수입니다.'.format(func.__name__, self.x))
            else:
                print('{0}의 반환값은 {1}의 배수가 아닙니다.'.format(func.__name__, self.x))
            
            return r
        return wrapper

@IsMultiple(3)    # 데코레이터(인수)
def add(a, b):
    return a + b
 
print(add(10, 20))
print(add(2, 5))

클래스로 데코레이터를 만들고 싶다면 클래스 내에 __call__() 함수를 만들어 사용할 수 있다. 입력 받고싶은 변수는 __init__()이나 위와 같이 @dataclass를 통해 관리할 수 있다(dataclass는 python 3.7부터 기본으로 제공해준다).

functools.wraps 활용

데코레이터를 사용할 때 생기는 문제점 중 하나는 함수의 메타 정보가 데코레이터의 메타 정보로 덮어씌어 진다는 것이다.

def decorate(func):
    def wrapper(*args, **kwargs):
        print(func.__name__, '함수 시작')
        func()                              
        print(func.__name__, '함수 끝')
    return wrapper

@decorate
def hello():
    print('hello')


print(hello)
print(hello.__name__)
<function decorate.<locals>.wrapper at 0x7f2a2a739820>
wrapper

이런 문제를 해결하기 위해 fucntools 모듈의 wraps 데코레이터를 활용할 수 있다.

from functools import wraps

def decorate(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(func.__name__, '함수 시작')
        func()                              
        print(func.__name__, '함수 끝')
    return wrapper

@decorate
def hello():
    print('hello')


print(hello)
print(hello.__name__)
<function hello at 0x7f2a2a8288b0>
hello

wrapper()함수 위에 @wraps 데코레이터를 씌워준 후 decorator()에서 받은 인자(여기서는 func)를 @wraps(func)와 같이 넣어주면 된다. 이렇게 해주면 hello()의 원래 정보가 정상적으로 나오는 것을 확인할 수 있다.

decorator 활용

@property

@property는 get함수와 set함수를 처리할 때 사용하는 데코레이터다. 사용 목적은

  1. 변수를 변경 할 때 어떠한 제한을 두고 싶어서
  2. get,set 함수를 만들지 않고 더 간단하게 접근하게 하기 위해서
  3. 하위호환성에 도움이 됨.

정도가 있다.

class Test:
    def __init__(self):
        self.color = "red"

    def set_color(self,clr):
        self.color = clr

    def get_color(self):
        return self.color
    
    
t = Test()
t.set_color("blue")

print(t.get_color())

위와 같은 color를 입력받고 반환하는 기능을 갖춘 클래스가 있다고 생각해보자. 함수 이름을 set_, get_로 다르게 함으로써 구분지었다. 함수 네이밍을 위와 같이하면 함수의 개수가 늘어날 시 굉장히 복작해지고, 사람마다 네이밍 규칙이 다르기에 혼동을 일으키기 쉽다. 아래의 예시를 통해 @property는 어떻게 작성하고 있는지 알아보자.

class Test:

    def __init__(self):
        self.__color = "red"

    @property
    def color(self):
        return self.__color

    @color.setter
    def color(self,clr):
        self.__color = clr


t = Test()
t.color = "blue"

print(t.color)

color()라는 공통된 이름을 사용하는 함수가 존재하는 걸 볼 수 있다. 하나는 @property, 다른 하나는 @setter를 사용하였다. @propery는 get의 역할을 수행하고 @setter는 set의 역할을 수행한다. @setter의 경우 @{함수이름}.setter의 방식으로 앞에 함수 이름을 적어줘야한다. 위에서는 color()함수에 데코레이터를 붙였기 때문에 @color.setter()로 적었다.

Reference