본문 바로가기

DEVELOP_NOTE/Python

Python Decorator @ 사용방법 완벽 이해하기!

오늘은 Python의 데코레이터를 사용하는 방법에 대해 정리해보자.

 

@decorator_ # <-- 요런거
def function():    
	print "what is decorator?"

Github등 오픈 소스 커뮤니티에 공유된 코드들을 읽다보면, 위와 같이 '@' 기호로 표시된 코드들을 자주 볼 수 있다.

오늘 알아보려하는 데코레이터는 보통 이 '@'기호로 정의된 함수 또는 클래스를 의미한다.

 

데코레이터를 간단히 설명하면,

"다른 함수를 감싸는 함수"로, 감싸진 함수에 추가적인 기능을 제공하거나, 감싸진 함수의 동작을 변경하는데 사용되는 기능이다.

But, 위 설명만 가지고는 사실 잘 이해가 되지않는다. 

 

우선은 '다른 함수를 감싸는 함수'라는 부분만 기억하고 넘어가자.

 

데코레이터...? 어떻게 사용하는거지?

 

데코레이터는 함수 형태로 사용하는 방법과 클래스로 사용하는 방법이 있다.

먼저, 함수로 데코레이터를 사용하는 방법에 대해 먼저 살펴보자.

 

1. 함수형태로 사용하는 방법

위에서 데코레이터는 함수를 감싸는 함수로써, 감싸진 함수에 추가적인 기능을 제공하거나, 동작을 변경하는데 사용된다고했다.

그 예시로 아래 코드를 살펴보자.

import time

def timer_decorator(func):
    def wrappers(*arg, **kwargs):
        start_time = time.time()
        result = func(*arg, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} 실행 시간: {end_time - start_time}초")
        return result
    return wrappers

@timer_decorator
def example_function():
    for _ in range(1000000):
        pass

example_function()


"""
[OUTPUT]
example_function 실행 시간: 0.04586601257324219초
"""

위 코드는 간단한 roof 함수를 데코레이터로 wrapping한 예시 코드인데, 위 코드에서 데코레이터가 동작하는 순서를 아래에 정리해보자.


(1) 가장 먼저, 'time_decorator'이라는 데코레이터 함수를 생성한다.

함수의 내용은, 인자로 받은 'func'함수를 실행하고, 함수 실행 이전과 이후에 시간을 체크하여, 함수의 실행시간을 측정하여 출력하는 

timer함수이다.

 

(2) 그 다음, 만들어진 데코레이터 함수를 통해 '감쌀 함수'로 'example_function'이라는 함수를 정의했다.

해당 함수는 100만번의 루프를 도는 간단한 함수이다.

 

(3) 그리고 해당 함수를 '@'를 통해 '@timer_decorator'함수로 대상 함수를 감싸주면 데코레이터 적용이 완료된다.


 

이렇게 함수를 구성하게 되면, 아래에서 'example_function()'을 통해 감싸진 함수를 호출할때, 

해당 함수는 '@timer_decorator'이라는 데코레이션 함수로 wrapping된 상태이기 때문에,

데코레이터 함수 'timer_decorator' 의 인자로 example_function함수가 들어가게 되고, 데코레이터 함수가 실행된다.

그리고, 데코레이터 함수내에서는, 인자로 받은 'example_fuanction'함수가 실행되고, 해당 함수의 실행시간이 측정되어

시간이 출력된다.

 

즉, 함수 실행은 'example_function()'로 호출했지만,

해당 함수가 decorator로 wrapping되어있었기 때문에, 

wrapping된 데코레이터 함수 내부에서 인자로 받은 'example_function'함수를 실행한 결과값을 반환하게 된 것이다.

 

이해가 되는가..?

 

 

여기서 데코레이터 함수를 구성할때, 주의해야할 점이 있다.

import datetime

def time_check(func):
    print(datetime.datetime.now())
    func()
    print(datetime.datetime.now())

@time_check
def output_func():
    print('이거 위아래로 시간을 찍어라')

output_func()

"""
[OUTPUT]
2024-02-07 11:57:21.361220
이거 위아래로 시간을 찍어라
2024-02-07 11:57:21.362256
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/2g/2b3fqv816cb5kxrlc3v553280000gn/T/ipykernel_10484/1157037113.py in <module>
     10     print('이거 위아래로 시간을 찍어라')
     11 
---> 12 output_func()

TypeError: 'NoneType' object is not callable
"""

 

위 데코레이터 함수는 언뜻 봤을때는 큰 문제가 없어보이지만, 오류가 발생한다.

오류와 더불어서 동작에도 문제가 있는데, 'output_func()'와 같이 output_func함수가 호출되는 시점에서,

데코레이터를 포함한 output_func 함수가 실행되어야 하지만,

위 출력된 결과값을 보면, 

'output_func()'가 호출되기 전에 이미,

2024-02-07 11:57:21.361220
이거 위아래로 시간을 찍어라
2024-02-07 11:57:21.362256

함수의 결과값이 출력되었고, 이후에 output_func() 호출과정에서 오류가 발생한 것을 확인할 수 있다.

 

이 오류와 관련해서 우리가 데코레이터를 사용함에 있어, 기억해야할 것은

데코레이터함수 내부에서 wrapping된 함수를 불러와 실행할때는 데코레이터 내부에 또다른 함수(wrapper), 즉 위에서 설명한 올바른 코드의 예시에서 'wrappers'함수와 같은 데코레이터 함수 내에, wrapper함수를 정의해서, 인자로 받은 외부 함수를 감싸야 한다는 것이다.

 

반드시, 데코레이터 함수 내부의 wrapper함수로 외부함수를 감싸야지만, 'output_func'함수가

'호출되는 시점'에 실행되는, 올바른 동작 형태를 가지게 된다.

 

이 부분은 데코레이터 함수를 사용하는 규칙이므로 사용방식을 이해하고 넘어가면 된다. 

 

 

동작방식은 ok... 그래서, 데코레이터를 왜 사용하는거지?

def example_function1():
    start_time = time.time()
    for _ in range(5000000):
        pass
    end_time = time.time()
    print(f"example_function1 실행 시간: {end_time - start_time}초")
    
def example_function2():
    start_time = time.time()
    for _ in range(5000000):
        pass
    end_time = time.time()
    print(f"example_function2 실행 시간: {end_time - start_time}초")
    
def example_function3():
    start_time = time.time()
    for _ in range(5000000):
        pass
    end_time = time.time()
    print(f"example_function3 실행 시간: {end_time - start_time}초")
    
example_function1()
example_function2()
example_function3()

"""
example_function1 실행 시간: 0.10071301460266113초
example_function2 실행 시간: 0.10005807876586914초
example_function3 실행 시간: 0.10119986534118652초
"""

 

만약 데코레이터를 사용하지 않고 위의 example_function1~3함수 각각 실행시간을 체크하려고 한다면,

아마도 위의 코드와 같이 각 함수별로 timer 코드를 중복으로 넣어줘야할것이다.

 

 

하지만, 데코레이터를 사용하면 각 함수에 중복으로 활용되는 코드를 반복적으로 입력할 필요없이,

별도로 정의된 데코레이터 함수를 통해 간결하고 가독성 좋은 코드를 작성할 수 있다.

아래 코드에서 데코레이터 활용시의 장점을 확인할 수 있다.

def timer_decorator(func):
    def wrapper(*arg, **kwargs):
        start_time = time.time()
        result = func(*arg, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} 실행 시간: {end_time - start_time}초")
        return result
    return wrapper

@timer_decorator
def example_function1():
    for _ in range(1000000):
        pass
    
@timer_decorator
def example_function2():
    for _ in range(10000000):
        pass
    
@timer_decorator
def example_function3():
    for _ in range(5000000):
        pass

example_function1()
example_function2()
example_function3()

"""
[OUTPUT]
example_function1 실행 시간: 0.02248668670654297초
example_function2 실행 시간: 0.2073652744293213초
example_function3 실행 시간: 0.10090112686157227초
"""

 

 

 

2. 클래스 형태로 사용하는 방법

함수로 데코레이터를 사용하는 방법을 이해했다면, 클래스로 사용하는 방법은 금방 이해할 수 있다.

클래스 데코레이터는,


1) 별도의 데코레이터 클래스를 만든다.

2) 데코레이터 클래스 내부의 __call__ 매서드 내부에, 데코레이터를 통해 실행하고자하는 코드를 넣어 정의한다.


나머지는 다 똑같다!

아래 코드에서 확인해보자.

class Timer_Decorator:
    def __init__(self, func):
        self.func = func
    def __call__(self, *arg, **kwargs):
        start_time = time.time()
        self.func(*arg, **kwargs)
        end_time = time.time()
        print(f'{self.func.__name__} 실행시간: {end_time-start_time}초')

class MainClass:
    @Timer_Decorator
    def example_function1():
        for _ in range(1000000):
            pass
        
    @Timer_Decorator
    def example_function2():
        for _ in range(10000000):
            pass
        
    @Timer_Decorator
    def example_function3():
        for _ in range(5000000):
            pass

this_class = MainClass()
this_class.example_function1()
this_class.example_function2()
this_class.example_function3()

"""
[OUTPUT]
example_function1 실행시간: 0.029875993728637695초
example_function2 실행시간: 0.19496917724609375초
example_function3 실행시간: 0.09695100784301758초
"""

 

설명한것과 같이 데코레이터 클래스의 __call__매서드 내부에, 데코레이터를 실행할 때 호출할 코드내용을 담는다.

데코레이터는 클래스명으로 정의하면 된다.

위 코드와 같이, 외부 클래스 내부에 속한 내장함수들의 위에 데코레이터(클래스)를 붙여주고, 

각각의 내장함수를 호출하면, wrapping된 데코레이터의 내용을 타고 출력물이 생성되는것을 확인할 수 있다.

위에서는 클래스 내부의 내장함수에 데코레이터를 붙이는 예시로 들었지만,

클래스로 생성한 데코레이터를 일반 함수에 바로 붙여도, 아래와 같이 동일하게 잘 동작한다.

class Timer_Decorator:
    def __init__(self, func):
        self.func = func
    def __call__(self, *arg, **kwargs):
        start_time = time.time()
        self.func(*arg, **kwargs)
        end_time = time.time()
        print(f'{self.func.__name__} 실행시간: {end_time-start_time}초')


@Timer_Decorator
def example_function1():
    for _ in range(1000000):
        pass

@Timer_Decorator
def example_function2():
    for _ in range(10000000):
        pass

@Timer_Decorator
def example_function3():
    for _ in range(5000000):
        pass

this_class = MainClass()
example_function1()
example_function2()
example_function3()

"""
[OUTPUT]
example_function1 실행시간: 0.03767681121826172초
example_function2 실행시간: 0.20968103408813477초
example_function3 실행시간: 0.09826517105102539초
"""

 

 

 

 

마치며

오늘은 파이썬 데코레이터에 대해서 알아보았다.

데코레이터는 잘만 사용하면 반복적으로 작성해야하는 코드들을 줄여주기때문에, 코드의 간결성을 높일 수 있고,

기능별로 정의하여 함수 및 클래스에 이식하여 사용할 수 있기 때문에

코드의 가독성을 높이고, 객체지향적 코드를 구현하는데도 좋은 기능이라고 생각된다.

다만, 데코레이터에는 별도의 추가 인자를 받아 처리하는 방법 등 생각보다 매우 다양한 방식의 활용 케이스들이 있다.

따라서, 위 기본적인 내용을 바탕으로 좀 더 여러 케이스들을 접하고 활용해보는 공부가 필요할 것 같다.

 

 

Reference

 

python decorator (데코레이터) 어렵지 않아요

Python decorator (데코레이터) Python 으로 작성된 Opensource 의 코드들을 보다 보면, 아래와 같이 @ 로 시작하는 구문 들을 볼 수 있다. @decorator_def function(): print "what is decorator?" decorator를 한마디로 얘기

bluese05.tistory.com