처음에 closure와 decorator의 개념을 듣고는 잘 이해가 가지 않았다. 따라서 이번 포스트를 통하여 closure(추후 decorator)의 개념을 정리하고자 한다.
기본적으로 파이썬은 function 안에 function이 호출되는 nested function이 가능하다고 한다. 많은 블로그들에서 pow함수를 예로 들어서 사용하였고, 아마 사용할 수 있는 예제 중에 간단한 예제이기 때문에 먼저 pow 함수를 예제로 사용하고자 한다. 먼저 우리는 pow 연산을 아래와 같이 수행할 수 있다.
result=5**3
혹은 아래와 같이 함수로 계산할 수도 있을 것이다.
>>> def calc_pow3(x):
>>> return x**3
>>> print(calc_pow(5))
125
물론 calc_pow(x,y)과 같이 x,y 인자를 받아서 연산을 수행할 수 있지만, 여기서는 x만 받아야 하는 경우라고 가정하도록 하겠다. 따라서 calc_pow3은 어떤 값을 입력받아서 세제곱의 값을 리턴하는 함수라고 할 수 있겠다. 하지만 4제곱, 5제곱을 수행하는 경우라면 calc_pow4, calc_pow5 등 계속하여 함수를 작성하여야 할 것이다. 이러한 경우에는 closure라는 개념을 사용하면 훨씬 편리할 것이다. closure는 간단히 nested function을 사용하여 계산할 수 있다.
>>> def calc_pow(y):
>>> def calc_inner(x):
>>> return x**y
>>> return calc_inner
>>>
>>> calc3=calc_pow(3)
>>> calc5=calc_pow(5)
>>> print(calc3(2), calc5(2))
8, 32
이 로직을 간략히 정리하면 아래와 같다.
- calc3은 calc_pow(3)을 호출하고, calc3은 y가 3으로 설정된 clac_inner를 리턴받는다.
- 따라서 calc3을 사용하면, y가 3으로 설정된 함수 calc_inner를 사용할 수 있게 되는 것이다.
- calc5 역시 y가 5로 설정된 calc_inner를 리턴받았고, calc5를 사용하면 5제곱 연산을 수행할 수 있다.
다시 다른 예제를 생각해보도록 하자. 아래와 같이 x값을 전달하면 이차식이 계산되는 함수를 가정하도록 하겠다.
result = a*x**2 + b*x + c
이를 함수로 작성하면 아래와 같이 될 것이다.
>>> def calc_formula(x,a,b,c):
>>> return a*x**2 + b*x + c
>>>
>>> print(calc_formula(3,2,3,4))
31
>>> print(calc_formula(4,2,3,4))
48
하지만 이를 closure로 계산을 하면,
>>> def calc_formula(a,b,c):
>>> def calc_inner(x):
>>> return a*x**2 + b*x + c
>>> return calc_inner
>>>
>>> calc=calc_formula(2,3,4)
>>> print(calc(3),calc(4))
31, 48
이렇게 작성하면 calc가 calc_formula(2,3,4)를 호출할 때 calc_formula의 지역변수인 a, b, c 값까지 calc가 전달받는다. a,b,c값은 calc_formula의 지역변수임에도 할당해제되지 않고 사용이 가능하다.
- 따라서 closure는 local variable과 function을 사용할 수 있는 기능(?)이라고 할 수 있을 것이다.
예를 들어 아래와 같이 closure에서 어떤 값을 count한다고 하여보자.
>>> def calc_count(num):
>>> count=num
>>> def calc_inner():
>>> count+=1
>>> print(count)
>>> return calc_inner
>>>
>>> c=calc_count(5)
>>> print(c())
UnboundLocalError: local variable 'count' referenced before assignment
- 위 코드 같은 경우에는 calc_inner가 count 변수에 값을 쓰고자 하였지만, calc_count의 지역변수이기 때문에 접근할 수 없다는 에러가 발생한다.
- 기본적으로 자식 함수는 부모 함수의 변수를 읽기만 가능하고 쓰기는 불가능하다.
- 이러한 경우에는 nonlocal을 사용하면 에러 해결이 가능하다.
>>> def calc_count(num):
>>> count=num
>>> def calc_inner():
>>> nonlocal count
>>> count+=1
>>> print(count)
>>> return calc_inner
>>>
>>> c=calc_count(5)
>>>
>>> c()
6
>>> c()
7
- 이렇게 nonlocal을 사용하면 부모 함수의 변수를 업데이트할 수 있는 것을 확인할 수 있다.
decorator는 closure와 유사하지만 함수를 다른 함수의 인자로 전달한다는 것에서 약간 다른 개념이다. decorator는 말 그대로 함수를 꾸며주는(?) 역할을 수행하며, decorator를 사용하면 함수의 변형 없이 기능을 추가할 수 있다.
>>> import time
>>> def time_to_sleep(sec):
>>> time.sleep(sec)
>>>
>>> time_to_sleep(1)
가령 위와 같이 주어진 시간동안 프로세스를 sleep시키는 함수가 있다고 예를 들어보겠다. 이 함수가 잘 작동하는 지를 알아보기 위해서는 함수의 실행시간동안 wall clock time을 측정하는 수밖에 없다. 따라서 일반적으로 아래와 같이 time.time()함수를 호출할 수 있다.
>>> import time
>>> def time_to_sleep(sec):
>>> start_time=time.time()
>>> time.sleep(sec)
>>> end_time=time.time()
>>> print("elapsed time:", end_time-start_time)
>>>
>>> time_to_sleep(1)
elapsed time: 1.000467300415039
혹은 함수 바깥에서 타이머를 측정할 수 있다.
>>> import time
>>> def time_to_sleep(sec):
>>> time.sleep(sec)
>>>
>>> start_time=time.time()
>>> time_to_sleep(1)
>>> end_time=time.time()
>>> print("elapsed time:", end_time-start_time)
elapsed time: 1.000467300415039
이렇게 시간을 측정하는 방법이 있으나 함수마다, 혹은 함수의 호출마다 코드를 추가하여야 한다는 단점이 있으므로 매우 불편하다. 따라서 이러한 경우에 decorator를 사용하는 것이 좋은 예가 될 것이다.
def timer_kimjy(func):
def wrapper(sec):
start_time=time.time()
func(sec)
end_time=time.time()
print("elapsed time: ",end_time-start_time)
return wrapper
@timer_kimjy
def time_to_sleep(sec):
time.sleep(sec)
이렇게 decorator를 사용하여 timer를 측정하면 보다 효율적으로 코드를 작성할 수 있다.
상황에 따라서 각 함수 별로 시간을 측정해야 하는 경우가 있는데, 이러한 경우에는 decorator를 사용하면 아래와 같은 코드가 될 것이다. 예제코드의 경우에는 전역변수를 사용하였지만, 전역변수를 사용하지 않고도 효율적인 코드를 작성할 수 있을 것이라 생각된다.
import time
timer_time=[0,0]
timer_count=[0,0]
def timer_kimjy(routine_name, order):
def check_decorator(func):
def wrapper(sec):
global timer_time, timer_count
start_time=time.time()
func(sec)
end_time=time.time()
timer_time[order]+=end_time-start_time
timer_count[order]+=1
if(timer_count[order]%10==0):
print("elasped time of (",routine_name,"):",timer_time[order]/timer_count[order])
return wrapper
return check_decorator
@timer_kimjy("sleep routine", 0)
def time_to_sleep(sec):
time.sleep(sec)
@timer_kimjy("bed routine",1)
def time_to_bed(sec):
time.sleep(sec)
for i in range(20):
time_to_sleep(0.1)
if(i%2==0):
time_to_bed(0.1)
>>> elasped time of ( sleep routine ): 0.10030982494354249
>>> elasped time of ( bed routine ): 0.10051407814025878
>>> elasped time of ( sleep routine ): 0.10035451650619506
이렇게 decorator를 사용하여 각 루틴별로 실행시간을 측정하고, 함수가 10회 호출될 때마다 평균소비시간을 출력하는 코드를 작성할 수 있을 것이다.