Decorator - (1) 함수 만들기

다음 두 피보나치 함수의 실행시간을 테스트해볼려고 한다.

# 피보나치 반복함수
def fibonacci_iterative(n):  
    prev_n, cur_n = 0, 1
    i = 1

    while i < n:
        cur_n, prev_n = cur_n + prev_n, cur_n
        i+=1
    return cur_n

# 피보나치 재귀함수
def fibonacci_recursive(n):  
    if n == 0 or n == 1:
        return n
    return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)

이를 위해 다음과 같이 함수와 n을 parameter로 갖는 함수의 실행시간을 측정하는 함수를 따로 만들었다.

import time

def get_timer(function, n):  
    start_time = time.time()
    ret = function(n)
    end_time = time.time()    
    print("실행시간은 {time}초입니다.".format(time=(end_time - start_time))
    return ret

이 함수를 이용하면 각 함수의 실행시간이 출력된다.

get_timer(fibonacci_iterative, 200)  
get_timer(fibonacci_recursive, 200)  

하지만 이렇게 하면,시간을 측정할 때마다 일일이 get_timer 함수에 parameter를 넣고 호출하여야 한다. 내가 원하는 함수를 실행했을 때 자동으로 실행시간이 출력되는 함수를 만들 순 없을까?
이것을 가능하게 해주는 것이 바로 Decorator이다. Decorator은 이름 그대로 객체를 꾸며주어 객체의 기능을 확장시켜주는 역할을 한다. 그리고 그 객체로는 함수나 클래스가 될 수 있다. 아까 get_timer 함수를 Decorator 함수로 만들어보자.

def get_timer_decorator(function):  
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = function(*args, **kwargs)
        end_time = time.time()
        print("실행시간은 {time}초 입니다.".format(time=(end_time - start_time)))
        return result
    return wrapper

함수 안에 함수가 있는 구조이다. 바깥에 감싸고 있는 함수를 외부 함수라고 하고, 안에 있는 함수를 내부 함수라고 한다. 외부 함수는 특정 함수를 parameter로 받아 내부 함수로 전달해준다. 그리고 내부 함수는 그 특정 함수의 parameter를 자신의 parameter로 받는다.
지금과 같은 경우에서는 피보나치 함수들이 하나의 parameter만 받으므로 wrapper 함수가 n이라는 하나의 parameter만 가져도 된다. 하지만 모든 함수들에 다 적용될 수 있도록 가변인자인 *args, **kwargs 로 parameter를 설정하자.(*args**kwargs에 관한 글은 *args와 **kwargs 에 포스팅하였다.)
이제 아까 만든 함수의 위에다가 Decorator 함수를 선언하기만 하면 된다.

# 피보나치 반복함수
@get_timer
def fibonacci_iterative(n):  
    prev_n, cur_n = 0, 1
    i = 1

    while i < n:
        cur_n, prev_n = cur_n + prev_n, cur_n
        i+=1
    return cur_n

# 피보나치 재귀함수
@get_timer
def fibonacci_recursive(n):  
    if n == 0 or n == 1:
        return n
    return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)

fibonacci_iterative(200)  
fibonacci_recursive(200)  

앞에 @를 붙여 해당하는 함수 위에 선언하면 함수가 실행될 때 Decorator 함수가 대신 이 함수를 실행해주게 된다. 그리고 그 함수와 함수의 parameter은 Decorator 함수의 외부 함수와 내부 함수의 parameter로 각각 들어간다. 여기서 주의해야할 점은 내부 함수는 종료될 때 반드시 자기 자신을 반환해주어야 한다.
이해를 돕기 위해 하나의 예제를 더 보자.

def fridge(function):  
    def wrapper(*args, **kwargs):
        print("냉장고를 연다.")
        function(*args, **kwargs)
        print("냉장고를 닫는다.")
    return wrapper

@fridge
def put_fridge(food):  
    print("{food}를 냉장고에 넣는다.".format(food = food))

put_fridge("족발")  

냉장고를 연다.
족발를 냉장고에 넣는다.
냉장고를 닫는다.

보통 특정 함수의 실행 전후에 추가적인 기능을 원할 때, Decorator 함수는 기존의 함수에다가 그 기능을 추가한 새로운 함수를 반환해준다.
그렇다면 Decorator 함수 여러 개를 하나의 함수에도 적용할 수 있을까?
가능하다. 단 그 때는 Decorator 함수들의 실행 순서에 주의해야 한다. 말로 설명하는 것보다 밑에 예제를 보면 Decorator 함수의 실행 순서를 한 눈에 볼 수 있다.

def one(function):  
    def wrapper(*args, **kwargs):
        print("function one start")
        ret = function(*args, **kwargs)
        print("function one end")
        return ret
    return wrapper

def two(function):  
    def wrapper(*args, **kwargs):
        print("function two start")
        ret = function(*args, **kwargs)
        print("function two end")
        return ret
    return wrapper

def three(function):  
    def wrapper(*args, **kwargs):
        print("function three start")
        ret = function(*args, **kwargs)
        print("function three end")
        return ret
    return wrapper

@one
@two
@three
def something():  
    print("function something")

something()  

출력결과는 아래와 같다.

function one start
function two start
function three start
function something
function three end
function two end
function one end

Decorator 함수에서 다시 원래 함수가 실행되기 때문에 일반적인 재귀함수가 실행되는 순서와 같다.