[Python] Decorator 데코레이터

Poke·2024년 4월 13일

Decorator

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는 메인함수의 중간에 끼어드는 구문은 추가할 수 없고 메인함수의 앞뒤에 추가적인 작업만 가능하다.

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                           

Decorator로부터 원래 함수의 리턴값 그대로 받기

@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 사용시 주의할 점

메타 데이터의 변경
함수를 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()를 호출할 수 있으며, 인스턴스나 클래스 변수에 접근하지 않는 함수를 클래스 내부에 포함시킬 때 유용.

class로 decorator 만들기

클래스를 활용할때는 인스턴스를 함수처럼 호출하게 해주는 __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

0개의 댓글