Python 데코레이터
파이썬을 사용하다보면 함수 또는 클래스 위에 @로 시작하는 녀석들을 볼 수 있다. 이러한 데코레이터를 사용하면 함수에 부적인 동작을 추가하거나 작동을 바꿀 수 있다. 잘 사용한다면 코드가 훨씬 깔끔해지기 때문에 어느정도 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함수를 처리할 때 사용하는 데코레이터다. 사용 목적은
- 변수를 변경 할 때 어떠한 제한을 두고 싶어서
- get,set 함수를 만들지 않고 더 간단하게 접근하게 하기 위해서
- 하위호환성에 도움이 됨.
정도가 있다.
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