Python - Decorator 이해하기

    목차
반응형

Decorator?

flask, dash 혹은 streamlit 등을 쓰다보면 함수 위에 `@` 기호를 쓸 일이 종종 있다.

# flask 예시
@app.route('/')
def index():
    pass
    
    
# dash 예시
@dashapp.callback(
    dash.Output('param_output', 'value_output'),
    dash.Input('param_input', 'value_input')
)
def make_fig(param_input):
    ...
    return output
    
    
# streamlit 예시
@st.cache_data
def load_file(url):
    pass

이렇게 함수 위에 있는 `@`를 Decorator라고 한다.

 

이 Decorator를 깊이 이해하기 위해서는 first class function, closure 같은 걸 알아야 하는데 난 잘 써먹는게 더 중요하다 생각하여 따로 작성하지는 않는다.

 

 

Decorator 함수

Decorator 함수를 간단한 예시와 함께 설명하고자 한다.

def logging_decorator(func):
    def wrapper():
        print(f"Calling function {func.__name__}")
        result = func()
        print(f"Function {func.__name__} returned {result}")
        return result
    return wrapper


def foo():
    print('Hello World!')
    return 0
    
foo = logging_decorator(foo)
foo()

함수를 인자로 받아 그 함수를 작동시키고 걸리는 시간을 측정하는 또다른 함수를 반환하는 `logging_decorator` 함수가 있다. 이제 `foo` 함수를 `logging_decorator`의 인자로 넣어주면 `logging_decorator`의 `wrapper`가 반환되는데 이를 다시 `foo` 변수에 넣어준다. 그러면 `foo`는 여전히 함수이기 때문에 `foo()`를 실행해주면 아래와 같은 결과가 나온다.

Calling function foo
Hello World!
Function foo returned 0

 

위 코드를 decorator를 이용하면 아래와 같이 바꿀 수 있다.

def logging_decorator(func):
    def wrapper():
        print(f"Calling function {func.__name__}")
        result = func()
        print(f"Function {func.__name__} returned {result}")
        return result
    return wrapper

@logging_decorator
def foo():
    print('Hello World!')
    return 0

foo()

데코레이터 함수는 동일한데 다른 건 `foo` 위에 `@logging_decorator`가 추가된 것 뿐으로, 표현 방식을 `@`를 이용하여 간단화 한 것이다.

 

 

Decorator에 인자 넣기

만약 함수에 인자가 있는 경우, 아래처럼 `*args, **kwargs`를 넣어주면 된다.

def logging_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function {func.__name__} with arguments {args} and {kwargs}")
        result = func(*args, **kwargs)
        print(f"Function {func.__name__} returned {result}")
        return result
    return wrapper

@logging_decorator
def add(a, b):
    return a + b

print(add(3, 4))
Calling function add with arguments (3, 4) and {}
Function add returned 7
7

 

풀어서 보면 인자 작동 방식을 이해하기 쉽다.

def logging_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function {func.__name__} with arguments {args} and {kwargs}")
        result = func(*args, **kwargs)
        print(f"Function {func.__name__} returned {result}")
        return result
    return wrapper


def add(a, b):
    return a + b

add = logging_decorator(add) # <- wrapper(*args, **kwargs)

print(add(3, 4))

`logging_decorator(add)`를 하면 호출되지 않은 `wrapper(*args, **kwargs)`가 나오기 때문에 `add` 함수에 인자를 넣을 수 있게 되는 것이다.

 

 

2개 이상의 Decorator 쓰기

Decorator를 2개 이상 쓸 수도 있는데, 이럴 때 순서가 중요하다. 당연하겠지만 선언한 순서에 따라 실행되는 순서가 달라지기 때문이다.

import time
import numpy as np

# 성능 측정을 위한 데코레이터
def timer_decorator(func):
    print(1)
    def timer_wrapper(*args, **kwargs):
        print(2)
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Function {func.__name__} took {end_time - start_time:.4f} seconds")
        return result
    return wrapper

# 데이터 전처리를 위한 데코레이터
def preprocess_decorator(func):
    print(3)
    def preprocess_wrapper(data, *args, **kwargs):
        print(4)
        # 데이터 전처리 (예: 결측값 처리, 정규화)
        data = np.array(data)
        data = np.nan_to_num(data)  # 결측값을 0으로 대체
        data = (data - np.mean(data)) / np.std(data)  # 정규화
        return func(data, *args, **kwargs)
    return wrapper

# 데이터 분석 함수에 데코레이터 적용
@timer_decorator
@preprocess_decorator
def analyze_data(data):
    print(5)
    return data.mean()
    
# 데이터 리스트 (일부 결측값 포함)
data = [1, 2, np.nan, 4, 5, np.nan, 7, 8, 9, 10]

# 함수 호출
result = analyze_data(data)
print(result)

예를 들어 함수 작동 시간을 측정하는 데코레이터와 데이터 전처리를 하는 데코레이터가 있다고 해보자. 그리고 데이터 분석을 하는 함수에 데코레이터를 적용한다면 위처럼 2개의 `@`가 붙는다.

그럼 출력 결과는 어떻게 될까?

3
1
2
4
5
Function wrapper took 0.0000 seconds
1.1102230246251565e-16

 

이것도 함수 형태로 풀어서 보면 이해하기 쉬워진다.

# 데코레이터 함수 생략

# 데이터 분석 함수 정의
def analyze_data(data):
    print(5)
    return data.mean()

# 데코레이터 적용
analyze_data = timer_decorator(preprocess_decorator(analyze_data))

1. `@` 순서대로 `timer_decorator`가 가장 바깥에, 그 다음 `preprocess_decorator`가 들어가고 마지막에 `analyze_data` 함수가 들어간다. 그러면 실행 순서는 안쪽부터(역방향)이다.

2. `analyze_data`는 선언만 된 상태이기 때문에 함수 내부가 실행되지 않는다.

3. `preprocess_decorator`는 `analyze_data` 함수를 인자로 받았기 때문에 실행이 되어 `3`이 출력된다. 하지만 내부 `preprocess_wrapper`는 선언만 된 상태이기 때문에 함수 내부는 실행되지 않는다.

4. `timer_decorator`는 3에서 나온 `preprocess_wrapper`를 인자로 받아 `1`이 출력되고 `analyze_data`라는 변수는 `timer_wrapper` 함수가 된다.

 

# 함수 호출
result = analyze_data(data)
print(result)

5. 데이터를 넣어 함수 호출을 하게 되면 `timer_wrapper`가 실행되어 `2`가 출력된다. 또, `timer_wrapper`는 `preprocess_wrapper`를 인자로 받았기 때문에 내부에서 `preprocess_wrapper`가 실행되고 `4`가 출력된다.

6. `preprocess_wrapper`는 `analyze_data`를 인자로 받았기 때문에 `analyze_data`가 실행되면서 `5`가 출력된다.

7. 마지막으로 `timer_wrapper`에서 걸린 시간을 출력한다.

 

728x90
반응형