Decorator(데코레이터)는 함수를 직접 수정하지 않고 기능을 추가할때 사용함. 일반적으로 @기호와 함께 사용되며, 함수 또는 메소드 위에 위치.
데코레이터는 기본적으로 함수를 인자로 받고 또 다른 함수를 반환하는 고차함수(higher-order function).
Decorator는 대상 함수를 wrapping하고, wrapping된 대상 함수 앞뒤에 추가적인 구문들로 꾸며 사용한다.
예를들어,
import time
def func_1():
print(f"시작시간 : {time.time}")
print("func_1() 함수 실행")
print(f"종료시간 : {time.time}")
def func_2():
print(f"시작시간 : {time.time}")
print("func_2() 함수 실행")
print(f"종료시간 : {time.time}")
def func_3():
print(f"시작시간 : {time.time}")
print("func_3() 함수 실행")
print(f"종료시간 : {time.time}")
기존 함수에 시작시간, 종료시간을 추가한다고 생각해보자. 모든 함수에 추가하다보니 반복되는 구문이 많다지다보니 점점 가독성이 떨어진다.
위에 코드에 데코레이터를 적용하면,
import time
def time_decorator(func): # 호출할 함수를 매개변수로 받음
def wrapper(): # 호출할 함수를 감싸는 함수
print(f"시작시간 : {time.time()}") # 추가된 기능
func() # 기존 함수
print(f"종료시간 : {time.time()}") # 추가된 기능
return wrapper # wrapper 함수 반환
def func_1():
print("func_1() 함수 실행")
def func_2():
print("func_2() 함수 실행")
def func_3():
print("func_3() 함수 실행")
time_decorator(func_1)() # 데코레이터에 호출할 함수를 넣음
time_decorator(func_2)()
time_decorator(func_3)()
@기호를 사용해보면,
import time
def time_decorator(func):
def wrapper():
print(f"시작시간 : {time.time}")
func()
print(f"종료시간 : {time.time}")
return wrapper
@time_decorator # @ 데코레이터
def func_1():
print("func_1() 함수 실행")
@time_decorator
def func_2():
print("func_2() 함수 실행")
@time_decorator
def func_3():
print("func_3() 함수 실행")
# 함수 호출
func_1()
func_2()
func_3()
Decorator 선언된 부분을 보면, 먼저 decorator 역할을 하는 함수를 정의(time_decorator())하고, 이 함수에 decorator가 적용될 함수(func)를 인자로 받는다. decorator 역할을 하는 함수 내부에 또 한번 함수(wrapper())를 선언하여 여기에 추가적인 기능을 선언해 준다. decorator함수에 내부함수를 return 해주면 된다.
메인함수들의 앞에 @를 붙여 decorator 역할을 하는 함수를 호출해 준다. 다만 decorator는 메인함수의 중간에 끼어드는 구문은 추가할 수 없고 메인함수의 앞뒤에 추가적인 작업만 가능하다.
위의 예로 든 함수는 인자로 아무것도 넣지 않았지만, 인자를 넘기는 함수에도 데코레이터를 선언해보자.
@time_decorator
def say_hi(msg):
print(msg)
msg = "안녕하세요"
say_hi(msg)
TypeError: wrapper() takes 0 positional arguments but 1 was given
에러가 발생하는 이유는, 데코레이터 내부의 wrapper() 함수가 원래 인자(msg)를 무시해버렸기 때문. 원래 함수에서 넘어온 인자를 그대로 데코레이터의 내부 함수로 넘기려면 *args와 **kwargs를 사용해야함.
import time
def time_decorator(func):
def wrapper(*args, **kwargs):
print(f"시작시간 : {time.time()}")
func(*args, **kwargs)
print(f"종료시간 : {time.time()}")
return wrapper
@time_decorator
def new_year(age):
print("나이를 계산합니다")
return age + 1
age = 20
result = new_year(age)
print(result)
시작시간 : 1713021285.350877
나이를 계산합니다
종료시간 : 1713021285.3508859
None
원래 함수에서 리턴한 값이 None으로 출력된다. 데코레이터의 wrapper() 함수에서 원래 리턴값을 그대로 보존해주지 않았기 때문. 원래 함수의 리턴값을 변수에 저장해두고 리턴해주어야한다.
import time
def time_decorator(func):
def wrapper(*args, **kwargs):
print(f"시작시간 : {time.time()}")
value = func(*args, **kwargs) # 리턴값을 변수에 저장
print(f"종료시간 : {time.time()}")
return value # 변수 리턴
return wrapper
메타 데이터의 변경
함수를 decorator로 감싸면, 함수의 메타데이터(함수의 이름, docstrings, 주석 등)가 변경될 수 있음. 데코레이터 내부에서 정의된 새로운 함수(wrapper)가 원래 함수를 감싸기 때문에 발생함. 예를들어, 데코레이터를 통해 my_function을 감쌀 경우, my_function.__name__을 조회하면 'wrapper'로 나타날 수 있음.
이를 방지하기 위해 functools 모듈의 wraps 함수를 사용할 수 있다. wraps는 데코레이터 내부의 wrapper 함수에 적용하여 원래 함수의 메타데이터를 wrapper 함수에 복사합.
import time
from functools import wraps # 추가
def time_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"시작시간 : {time.time()}")
result = func(*args, **kwargs)
print(f"종료시간 : {time.time()}")
return result
return wrapper
@time_decorator
def new_year(age):
"""나이를 계산합니다"""
return age + 1
print(new_year.__name__)
print(new_year.__doc__)
new_year
나이를 계산합니다
decorator의 실행순서
여러 decorator를 하나의 함수에 적용할 때는 데코레이터의 실행 순서를 주의해야함. 데코레이터는 함수 정의에 가장 가까운 것부터 먼저 적용되며, 이후 바깥쪽으로 순차적으로 적용됨.
@decorator3
@decorator2
@decorator1
def some_function():
pass
위 코드에서 some_function에 적용된 데코레이터의 실행 순서는 decorator1 -> decorator2 -> decorator3 순서임.
함수의 반환값이나 매개변수 수정할 때 주의해야함.
네이버 API사용시 @staticmethod
아래 코드는 네이버 API 사용시 예제 코드임.
import hashlib
import hmac
import base64
class Signature:
@staticmethod
def generate(timestamp, method, uri, secret_key):
message = "{}.{}.{}".format(timestamp, method, uri)
hash = hmac.new(bytes(secret_key, "utf-8"), bytes(message, "utf-8"), hashlib.sha256)
hash.hexdigest()
return base64.b64encode(hash.digest())
코드에서 @staticmethod 데코레이터는 파이썬에서 클래스 내부의 메소드를 정적 메소드(staticmethod)로 선언할 때 사용함. 정적 메소드는 클래스나 인스턴스이 상태(속성)에 접근하지 않기 때문에 메소드 내에서 self나 cls를 사용할 수 없음. 정적 메소드는 그 클래스의 인스턴스 없이도 Signature.generate()를 호출할 수 있으며, 인스턴스나 클래스 변수에 접근하지 않는 함수를 클래스 내부에 포함시킬 때 유용.
클래스를 활용할때는 인스턴스를 함수처럼 호출하게 해주는 __call__메소드를 구현해야함.
import time
class TimeDecorator:
def __init__(self, func): # 호출할 함수를 인스턴스의 초기값으로 받음
self.func = func # 호출할 함수를 속성 func에 저장
def __call__(self):
print(f"시작시간 : {time.time()}")
self.func() # 속성 func에 저장된 함수를 호출
print(f"종료시간 : {time.time()}")
@TimeDecorator # 데코레이터
def func():
print("func() 함수 실행")
func()
클래스로 만든 데코레이터에도 원래함수의 인자를 처리할 수 있다.
import time
class TimeDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs): # 호출할 함수의 매개변수를 처리
print(f"시작시간 : {time.time()}")
result = self.func(*args, **kwargs) # 매개변수를 넣어서 호출하고, 반환값을 변수에 저장
print(f"종료시간 : {time.time()}")
return result
@TimeDecorator
def new_year(age):
print("나이를 계산합니다")
return age + 1
age = 20
result = new_year(age)
print(result)
매개변수가 있는 데코레이터 만들기.
두 수(a, b)의 합이 3의 배수인지 확인해보는 예제.
class IsMultiple:
def __init__(self, num): # 데코레이터가 사용할 매개변수를 초깃값으로 받음.
self.num = num
def __call__(self, func): # 호출할 함수를 매개변수로 받음
def wrapper(a, b): # 호출할 함수의 매개변수와 똑같이 지정
result = func(a, b)
if result % self.num == 0: # func의 반환값이 num의 배수인지 확인
print(f'{func.__name__}의 반환값은 {self.num}의 배수이다')
else:
print(f'{func.__name__}의 반환값은 {self.num}의 배수가 아니다')
return result
return wrapper
@IsMultiple(3) # 결과가 3의 배수인지 확인
def add(a, b):
return a + b
# 함수호출
print(add(10, 20))
add의 반환값은 3의 배수이다
30
추가로 공부할것
first class 함수
closer