오늘은 Python의 Closure(클로저) 와 Decorator(데코레이터) 에 대해 정리한다.
Python에서 함수를 호출하면 해당 함수만의 Local Namespace가 만들어진다.
def example(x):
y = x + 1
return y
example()이 실행되는 동안에는 x, y 같은 지역 변수가 존재하지만,
함수 실행이 끝나면 일반적으로 해당 호출의 Local Namespace도 더 이상 유지할 필요가 없어진다.
그렇다면 이런 상황은 어떨까?
💡 함수가 실행될 때 만들어진 상태를 나중에도 기억해서 사용하고 싶다면?
이럴 때 사용할 수 있는 개념이 Closure다.
Closure는 쉽게 말하면,
내부 함수가 자신이 만들어질 당시 외부 함수의 변수를 기억하는 구조
라고 이해할 수 있다.
def outer(x):
def inner():
return x
return inner
사용해보면 다음과 같다.
func = outer(10)
print(func())
# 10
outer(10)의 실행은 이미 끝났지만,
반환된 inner 함수는 자신이 사용하고 있는 x=10을 계속 기억한다.
내부 함수가 실제로 참조하는 외부 변수(Free Variable) 를 Closure가 기억한다.
즉,
def outer(x):
a = 100
def inner():
return x
return inner
위 코드에서 inner()는 x만 사용하므로,
Closure에서 중요한 값은 x다.
Closure를 사용하면 함수가 특정 상태를 기억하도록 만들 수 있다.
def make_multiplier(n):
def multiply(x):
return x * n
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10))
# 20
print(triple(10))
# 30
double은 n=2,
triple은 n=3이라는 상태를 각각 기억한다.
🎯 Closure의 핵심
함수가 만들어질 당시의 상태를 기억하고,
이후 호출에서도 그 상태를 계속 사용할 수 있다.
이 덕분에 전역 변수를 사용하지 않고도
함수마다 독립적인 상태를 유지할 수 있다.
Closure를 활용하면 다른 함수를 입력받아서 기능을 추가한 새로운 함수를 반환할 수도 있다.
예를 들어 함수 실행 시간을 측정해보자.
import time
def time_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
total_time = time.time() - start_time
print(f"time : {total_time}")
return result
return wrapper
그리고 원래 함수가 있다고 하자.
def custom_add(x, y):
print(x + y)
return x + y
이 함수를 time_decorator에 전달한다.
external_func = time_decorator(custom_add)
external_func(4, 4)
실행 흐름은 다음과 같다.
custom_add
↓
time_decorator(custom_add)
↓
wrapper 반환
↓
external_func = wrapper
↓
external_func(4, 4)
↓
wrapper(4, 4) 실행
↓
custom_add(4, 4) 실행 + 실행시간 측정
여기서 wrapper는 외부 함수인 time_decorator()가 끝난 뒤에도
func, 즉 custom_add를 기억하고 있다.
따라서 wrapper는 Closure다.
💡
wrapper라는 이름은 Python의 특별한 문법이 아니라,
기존 함수를 감싸는 함수에 관례적으로 자주 사용하는 이름이다.
현재 방식에서는 새로운 기능을 적용할 때마다 아래와 같이 작성해야 한다.
external_func = time_decorator(custom_add)
external_func(4, 4)
Python에서는 이 패턴을 더 명확하고 간결하게 표현하기 위해
Decorator 문법을 제공한다.
Decorator는 한 문장으로 정리하면,
기존 함수의 코드를 직접 수정하지 않고, 그 함수를 감싸서 새로운 기능을 추가하는 방법
이다.
다음과 같이 사용할 수 있다.
@time_decorator
def custom_mul(x, y):
print(x * y)
return x * y
이 코드는 사실 다음 코드와 동일한 의미다.
def custom_mul(x, y):
print(x * y)
return x * y
custom_mul = time_decorator(custom_mul)
즉,
@time_decorator
는 단순히 보기 좋게 만드는 문법만이 아니라,
custom_mul을 time_decorator에 전달하고,
반환된 새로운 함수로 custom_mul을 교체한다
는 의미다.
이제 다음과 같이 호출하면 된다.
custom_mul(3, 4)
실제로는 time_decorator에서 반환된 wrapper()가 실행되고,
그 안에서 원래 custom_mul()이 실행된다.
*args, **kwargs를 사용할까?Decorator는 어떤 함수에 적용될지 미리 알 수 없다.
예를 들어,
def add(x, y):
...
일 수도 있고,
def user_info(name, age, city="Seoul"):
...
일 수도 있다.
따라서 모든 형태의 인자를 받을 수 있도록 보통 다음과 같이 작성한다.
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
*args : 위치 인자**kwargs : 키워드 인자이렇게 작성하면 다양한 함수에 동일한 Decorator를 적용할 수 있다.
함수형 Decorator는 보통 Closure 구조를 사용한다.
def decorator(func):
def wrapper(*args, **kwargs):
# 실행 전 추가 기능
result = func(*args, **kwargs)
# 실행 후 추가 기능
return result
return wrapper
wrapper()는 외부 함수의 func를 기억하고 있으므로 Closure다.
즉,
Decorator 자체가 Closure인 것은 아니지만,
함수형 Decorator를 구현할 때 Closure가 매우 자주 사용된다.
Decorator가 기억해야 할 상태가 많아지면
함수형 Closure 대신 Class를 사용할 수도 있다.
예를 들어 함수가 몇 번 호출되었는지 기록하는 Decorator를 만들어보자.
class CallCounter:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.count}번째 호출")
return self.func(*args, **kwargs)
사용 방법은 함수형 Decorator와 동일하다.
@CallCounter
def hello(name):
print(f"Hello {name}")
hello("Min")
hello("Max")
출력 결과는 다음과 같다.
1번째 호출
Hello Min
2번째 호출
Hello Max
@CallCounter는 사실 다음과 같다.
hello = CallCounter(hello)
따라서 hello는 이제 원래 함수가 아니라
CallCounter 객체를 가리킨다.
하지만 CallCounter 클래스에 __call__()이 구현되어 있기 때문에
hello("Min")
처럼 함수처럼 호출할 수 있다.
단순한 기능이라면 함수형 Decorator로 충분하다.
하지만 관리해야 할 상태가 많아진다면 Class가 더 편할 수 있다.
self.func
self.count
self.config
self.logger
이런 상태들을 self.xxx 형태로 관리할 수 있고,
필요하다면 추가 메서드도 만들 수 있다.
| 상황 | 추천 방식 |
|---|---|
| 간단한 기능 추가 | 함수형 Decorator |
| 간단한 상태 기억 | Closure |
| 여러 상태를 관리 | Class Decorator |
| 여러 메서드와 복잡한 동작 필요 | Class Decorator |
🎯 Class Decorator만 할 수 있는 특별한 기능이라기보다는,
복잡도가 높아질수록 Class로 구조화하는 것이 관리하기 편해지는 것이다.
외부 함수 실행
↓
내부 함수 생성
↓
내부 함수가 외부 변수 참조
↓
내부 함수 반환
↓
외부 함수가 끝난 이후에도 필요한 값을 기억
함수가 자신이 만들어질 당시의 외부 상태를 기억할 수 있게 해주는 구조다.
원래 함수
↓
Decorator에 전달
↓
기능을 추가한 함수 또는 객체 반환
↓
원래 함수 이름에 다시 연결
기존 함수 코드를 수정하지 않고 공통 기능을 추가하는 방법이다.
대표적으로 다음과 같은 기능에서 사용할 수 있다.
💡 Closure는 함수가 상태를 기억하게 만드는 방법이고, Decorator는 그 구조 등을 활용하여 기존 함수에 공통 기능을 추가하는 패턴이다.
실제 Decorator를 작성할 때는 원래 함수의 이름과 문서 정보가 유지되도록
functools.wraps를 사용하는 경우가 많다.
from functools import wraps
import time
def time_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
print(f"time : {time.time() - start_time}")
return result
return wrapper
이 부분은 Decorator의 기본 원리를 이해한 뒤 추가로 공부하면 된다.