python

closure & decorator

kimjy 2021. 12. 15. 15:22

처음에 closure와 decorator의 개념을 듣고는 잘 이해가 가지 않았다. 따라서 이번 포스트를 통하여 closure(추후 decorator)의 개념을 정리하고자 한다.

 

closure

기본적으로 파이썬은 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을 사용할 수 있는 기능(?)이라고 할 수 있을 것이다.

 

nonlocal 표현 사용

예를 들어 아래와 같이 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

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회 호출될 때마다 평균소비시간을 출력하는 코드를 작성할 수 있을 것이다.