점프 투 파이썬 : https://wikidocs.net/book/1
파이썬 기본을 갈고 닦자 : https://wikidocs.net/16031
코딩 도장 : https://dojang.io/mod/page/view.php?id=2378
GeeksforGeeks : https://www.geeksforgeeks.org/decorators-in-python/
java로 개발을 하다보면 decorator 문법을 많이 사용하게 되는데, python에서도 decorator문법을 지원한다.
decorator문법은 특별한 것은 아니고 이전에 사용했던 @staticmethod
, @classmethod
, @absctractmethod
등과 같이, 함수 위에 얹혀서 사용되는 것을 말한다.
decorator문법을 사용하는 가장 큰 이유는 기존에 정의된 함수의 내용을 수정하지 않고 추가적인 내용, 기능을 덧붙일 수 있기 때문이다.
def process(a, b):
ret = a + b
print("ret: " + str(ret))
가령, process
함수의 시작
, 끝
을 기록하는 로깅을 하고 싶다고 하자. 그렇다면 다음과 같이 써줄 수 있다.
def start():
print("func started!")
def end():
print("func completed!")
def process(a, b):
start()
ret = a + b
print("ret: " + str(ret))
end()
process(1,2)
func started!
ret: 3
func completed!
다음과 같이 process
함수의 앞 뒤 부분에 start
, end
함수를 넣어주는 수 밖에 없다. 그러나, process
와 같은 함수들이 많아지면 많아질수록 이러한 방식은 매우 손이 많이 가는 일이며, 또한 process
라는 함수가 가진 concern이 훼손될 수 있다. concern은 함수가 관장하고 있는 관심사로 process
는 계산이라는 concern에만 집중하면 되는데 여기서는 logging이라는 로직도 들어가게되어 code를 읽는데 있어 가독성이 떨어질 수 있다.
python의 함수에는 다음과 같은 특성이 있다.
이 두가지 특성을 이용하여 위 문제를 해결해보자.
def logging(func, a, b):
def start():
print("func started!")
def end():
print("func completed!")
def wrapper():
start()
func(a, b)
end()
return wrapper
def process(a, b):
ret = a + b
print("ret: " + str(ret))
log_process = logging(process, 1 ,2)
log_process()
func started!
ret: 3
func completed!
기존에 만들었던 process
는 아무런 변경이 없고, 이 함수를 wrapping하도록 하여 log 기능을 추가하였다. 이것이 가능한 이유는 python은 함수를 매개변수로 받을 수 있고, 또는 반환이 가능하다는 것과 함수 안에 inner 함수를 선언할 수 있다는 것이다.
logging함수는 매개변수로 func
을 받는다. 이 func
을 실제로 wrapping하는 함수는 wrapper
이다. wrapper
는 logging
함수의 inner 함수이고, 반환값이다. 입력으로 받은 func
앞 뒤에 start
, end
를 넣은 wrapper
함수를 반환하고 이 함수를 log_process
로 받아 실행하면 원하는 결과가 나오게 되는 것이다.
위의 패턴을 간단하게 만들어주는 것이 바로 Decorator
이다. decorator는 decorator함수
와 이를 호출하는 @함수
가 있다. wrapper
를 반환하는 logging
함수가 바로 decorator함수
가 되고, @함수
는 process
위에 적어주기만 하면 된다.
process
를 a+b
로직이 아니라, 단순 print
만 하도록 로직을 변경하고, decorator를 적용한 코드를 만들어보자.
def logging(func):
def start():
print("func started!")
def end():
print("func completed!")
def wrapper():
start()
func()
end()
return wrapper
@logging
def process():
print("start!")
process()
func started!
start!
func completed!
굉장히 간단하다. logging
은 decorator 함수
이고, @logging
이 바로 @함수
로 process
함수 위에 적어주면 process
함수를 입력으로 받아 wrapper
가 반환되어 실행된다.
재밌는 것은 decorator를 여러개 지정할 수 있다는 것이다. 가령, 이번에는 시간을 관리하는 코드를 넣겠다고 한다면 다음과 같이 할 수 있다.
from datetime import datetime
def logging(func):
def wrapper():
print("func started!")
func()
print("func completed!")
return wrapper
def time_logging(func):
def wrapper():
now = datetime.now()
print("start Time =" ,now.strftime("%H:%M:%S"))
func()
now = datetime.now()
print("end Time =" ,now.strftime("%H:%M:%S"))
return wrapper
@time_logging
@logging
def process():
print("start!")
process()
start Time = 16:17:03
func started!
start!
func completed!
end Time = 16:17:03
이렇게 여러개의 decorator가 하나의 함수에 얹혀서 사용될 수 있는 이유는 함수를 일급 함수, 일급 객체로 취급하여 매개변수로 받고, 반환이 가능하기 때문이다. 사실 위의 process()
코드는
wrapper(wrapper(process))
와 같다.
매개변수를 decorator 함수에 넣어주는 방법은 decorator 함수의 반환 함수인 wrapper 함수에 똑같이 매개변수를 추가해주면 된다. 반환값 역시도 마찬가지이다. wrapper 에서 return해줄 값을 써주면 된다.
from datetime import datetime
def logging(func):
def wrapper(a,b): # 매개변수 a, b 가 추가됨
print("func started!")
ret = func(a,b)
print("func completed!")
return ret*2 # 반환값으로 process에 2를 곱한 값을 반환한다.
return wrapper
@logging
def process(a, b):
ret = a + b
print("start! ", ret)
return ret
double_value = process(10,2)
print(double_value)
func started!
start! 12
func completed!
24
위와 같이 wrapper로 반환되는 함수에 매개변수만 추가하면 process
에 들어간 매개변수 a, b를 사용할 수 있다. 또한, process
의 반환값에 두 배를 곱하여 반환하고 싶다면 wrapper의 반환값으로 process
의 반환값에 2를 곱해주면 된다.
그런데 만약, 가변 인수인 경우는 어떻게 해야할까?? 이는 wrapper 함수 역시도 가변 인수 함수로 만들어주면 된다. 참고로 가변 인수와 가변 키워드 인수를 소개하면 다음과 같다.
*
을 붙이며 튜플 형식으로 값이 들어간다. 이를 packing이라고 한다.*
을 붙이면 튜플 형식의 값이 풀어져서 함수의 매개변수로 들어간다. 이를 unpacking이라고 한다.**
을 붙이며 dictionary 형식으로 값이 들어간다. 이를 packing이라고 한다.**
을 붙이면 dictionary 형식의 값을 unpacking 하여 넣는다.from datetime import datetime
def logging(func):
def wrapper(*args, **kwargs): # 가변 인수 매개변수 args와 가변 키워드 매개변수 kwargs를 추가한다.
print("func started!")
ret = func(*args, **kwargs)
print("func completed!")
return ret
return wrapper
@logging
def get_min(*args):
return min(args)
@logging
def get_max(**kwargs):
return max(kwargs.values())
print(get_min(10,20,30))
print(get_max(x=2,y=20,z=24))
func started!
10
func started!
func completed!
24
다음과 같이 문제 없이 구동되는 것을 확인할 수 있다.
메서드에 decorator을 사용할 때는 주의해야한다. 클래스를 만들면서 메서드에 decorator를 사용할 때는 self에 주의해야 한다. 클래스의 인스턴스 메서드는 항상 self를 받으므로 decorator를 만들 때에도 wrapper 함수의 첫 번째 매개변수는 self로 지정해야 한다. 또한, 클래스 메서드도 마찬가지로 cls를 사용하므로 이를 decorator에 넣어야 한다.
def logging(func):
def wrapper(self, a, b):
print("start!")
r = func(self, a, b)
print("end!")
return r
return wrapper
class Calc:
@logging
def add(self, a , b):
return a + b
c = Calc()
print(c.add(10,20))
start!
end!
30
매개변수가 있는 데코레이터를 만드려면, wrapper함수 위를 덮고 있던 decorator 함수 위에 또 하나의 함수를 wrapping해야주야 한다. 즉, 다음과 같은 구조가 된다.
def para_decorator(x): # decorator의 파라미터를 받는 wrapper
def decorator(func): # decorator
def wrapper(a,b): # wrapper
#...
r = func(a,b)
#...
return r
return wrapper
return decorator # decoraotr를 반환한다.
@para_decorator(10) # decorator에 바로 매개변수를 써주면 된다.
def process(a,b):
#...
decorator에 파라미터가 존재하게되면 이제 decorator는 3단계로 이루어지게 된다. 첫번재 단계는 매개변수를 받는 wrapper, 두 번째 단계는 wrapping할 함수를 받는 wrapper, 세 번째 단계는 wrapping할 함수를 wrap하는 wrapper이다. 조금은 복잡해보여도 규칙은 간단하다.
이제 매개변수가 있는 decorator에 매개변수를 넣어보자, @decorator(value)
이런식으로 넣어주면 된다. 만약 keyword라면 keyword를 넣어주면 된다.
이를 활용하여 decorator로 넣은 매개변수로 결과값을 곱해주는 로직을 만들어보자.
def multiple_by(x):
def decorator(func):
def wrapper(a, b):
print("start!")
r = func(a, b)
print("end!")
return r * x
return wrapper
return decorator
@multiple_by(10)
def process(a, b):
return a + b
print(process(10,2))
start!
end!
120
다음과 같이 활용할 수 있다.