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`에서 걸린 시간을 출력한다.
'파이썬 Python' 카테고리의 다른 글
7판 4선승제에서 승리 확률 계산하는 코드 (python) (0) | 2024.05.08 |
---|---|
python으로 제곱근(root) 계산하는 함수 구현하기 (0) | 2024.04.26 |
wtf python 알아두면 좋은 내용 정리 (0) | 2024.01.31 |
Python 모르고 놓치고 있는 유용한 기능 (0) | 2024.01.30 |
객관식 문제 채점 프로그램 (python) (1) | 2021.03.23 |