파이썬 – 데코레이터 (Decorator)
이번 글에서는 파이썬의 데코레이터(Decorator)를 정리합니다. 본문을 이해하려면 퍼스트클래스 함수와 클로저에 대한 이해가 선행되어야 합니다. 두 개념이 익숙하지 않다면 다음 글을 먼저 참고합니다.
파이썬 – 퍼스트클래스 함수 (First Class Function)
파이썬 – 클로저 (Closure)
데코레이터란 무엇일까요? 사전적 의미로는 “장식가” 또는 “인테리어 디자이너"입니다. 이름 그대로, 기존 코드에 여러 가지 기능을 덧붙여 사용하는 파이썬 구문으로 이해하면 됩니다. 데코레이터 개념 자체는 단순하므로 예제를 보면서 사용법을 익혀봅니다.
먼저 작업 디렉터리에 decorator.py 파일을 만들고 다음 코드를 저장합니다.
def outer_function(msg):
def inner_function():
print(msg)
return inner_function
hi_func = outer_function('Hi')
bye_func = outer_function('Bye')
hi_func()
bye_func()저장 후 터미널에서 해당 디렉터리로 이동해 다음 명령어로 프로그램을 실행합니다.
$ python decorator.py
Hi
Bye앞서 다룬 파이썬 – 클로저 글을 읽었다면 위 코드의 동작 방식이 익숙할 것입니다. 데코레이터 코드도 큰 흐름은 비슷합니다. 다만 함수를 다른 함수의 인자로 전달한다는 점이 다릅니다. 가장 단순한 데코레이터 예제부터 살펴봅니다. 다음 코드를 저장해 실행합니다.
def decorator_function(original_function): # 1
def wrapper_function(): # 5
return original_function() # 7
return wrapper_function # 6
def display(): # 2
print('display 함수가 실행됐습니다.') # 8
decorated_display = decorator_function(display) # 3
decorated_display() # 4$ python decorator.py
display 함수가 실행됐습니다.위 코드의 동작 흐름은 다음과 같습니다. 데코레이터 함수 decorator_function과 일반 함수 display를 #1과 #2에서 각각 정의했습니다. #3에서는 decorated_display 변수에 display 함수를 인자로 받은 decorator_function의 리턴값을 할당합니다. 이 리턴값은 wrapper_function입니다. 이 시점에서 wrapper_function은 아직 실행된 상태가 아니며, decorated_display 변수에 담긴 채 호출을 기다리는 상태입니다. 그리고 #4의 decorated_display()로 wrapper_function이 실제 호출되면, #5에서 정의된 wrapper_function이 실행되고, #7에서 original_function인 display가 호출되며, 최종적으로 #8의 print로 문자열이 출력됩니다.
복잡해 보이는 데코레이터를 사용하는 이유는 도입부에서 설명했듯이, 이미 만들어진 기존 코드를 수정하지 않고도 wrapper 함수를 통해 새로운 기능을 덧붙일 수 있기 때문입니다. 예제로 살펴봅니다.
def decorator_function(original_function):
def wrapper_function():
print('{} 함수가 호출되기전 입니다.'.format(original_function.__name__))
return original_function()
return wrapper_function
def display_1():
print('display_1 함수가 실행됐습니다.')
def display_2():
print('display_2 함수가 실행됐습니다.')
display_1 = decorator_function(display_1) # 1
display_2 = decorator_function(display_2) # 2
display_1()
print()
display_2()$ python decorator.py
display_1 함수가 호출되기전 입니다.
display_1 함수가 실행됐습니다.
display_2 함수가 호출되기전 입니다.
display_2 함수가 실행됐습니다.위 예제처럼 하나의 데코레이터 함수로 display_1과 display_2 두 함수에 동일한 기능을 추가할 수 있습니다. 다만 실무에서는 #1, #2와 같은 구문은 잘 사용하지 않고, @ 심볼과 데코레이터 함수 이름을 붙여 쓰는 간결한 구문을 사용합니다. 위 코드는 다음과 같이 간소화할 수 있습니다.
def decorator_function(original_function):
def wrapper_function():
print('{} 함수가 호출되기전 입니다.'.format(original_function.__name__))
return original_function()
return wrapper_function
@decorator_function # 1
def display_1():
print('display_1 함수가 실행됐습니다.')
@decorator_function # 2
def display_2():
print('display_2 함수가 실행됐습니다.')
# display_1 = decorator_function(display_1) # 3
# display_2 = decorator_function(display_2) # 4
display_1()
print()
display_2()$ python decorator.py
display_1 함수가 호출되기전 입니다.
display_1 함수가 실행됐습니다.
display_2 함수가 호출되기전 입니다.
display_2 함수가 실행됐습니다.#3과 #4 대신 #1과 #2에 @ 심볼을 사용한 데코레이터 구문으로 코드가 한층 간결해졌습니다.
그런데 다음 코드의 display_info 함수처럼 인수를 가진 함수를 데코레이팅하려면 어떻게 해야 할까요? 파일을 수정해 실행해 봅니다.
def decorator_function(original_function):
def wrapper_function():
print('{} 함수가 호출되기전 입니다.'.format(original_function.__name__))
return original_function()
return wrapper_function
@decorator_function
def display():
print('display 함수가 실행됐습니다.')
@decorator_function
def display_info(name, age):
print('display_info({}, {}) 함수가 실행됐습니다.'.format(name, age))
display()
print()
display_info('John', 25)$ python decorator.py
display 함수가 호출되기전 입니다.
display 함수가 실행됐습니다.
Traceback (most recent call last):
File "decorator.py", line 21, in <module>
display_info('John', 25)
TypeError: wrapper_function() takes 0 positional arguments but 2 were givenwrapper_function은 인자를 받지 않는데 2개의 인자가 전달되었다는 타입에러가 발생했습니다. 다음과 같이 코드를 수정하면 문제가 해결됩니다.
def decorator_function(original_function):
def wrapper_function(*args, **kwargs): #1
print('{} 함수가 호출되기전 입니다.'.format(original_function.__name__))
return original_function(*args, **kwargs) #2
return wrapper_function
@decorator_function
def display():
print('display 함수가 실행됐습니다.')
@decorator_function
def display_info(name, age):
print('display_info({}, {}) 함수가 실행됐습니다.'.format(name, age))
display()
print()
display_info('John', 25)$ python decorator.py
display 함수가 호출되기전 입니다.
display 함수가 실행됐습니다.
display_info 함수가 호출되기전 입니다.
display_info(John, 25) 함수가 실행됐습니다.#1과 #2에 인수를 추가하여 문제를 해결하였습니다.
데코레이터는 함수 형식뿐 아니라 클래스 형식으로도 작성할 수 있습니다. 다음 예제로 살펴봅니다.
# def decorator_function(original_function):
# def wrapper_function(*args, **kwargs):
# print '{} 함수가 호출되기전 입니다.'.format(original_function.__name__)
# return original_function(*args, **kwargs)
# return wrapper_function
class DecoratorClass: # 1
def __init__(self, original_function):
self.original_function = original_function
def __call__(self, *args, **kwargs):
print('{} 함수가 호출되기전 입니다.'.format(self.original_function.__name__))
return self.original_function(*args, **kwargs)
@DecoratorClass # 2
def display():
print('display 함수가 실행됐습니다.')
@DecoratorClass # 3
def display_info(name, age):
print('display_info({}, {}) 함수가 실행됐습니다.'.format(name, age))
display()
print()
display_info('John', 25)$ python decorator.py
display 함수가 호출되기전 입니다.
display 함수가 실행됐습니다.
display_info 함수가 호출되기전 입니다.
display_info(John, 25) 함수가 실행됐습니다.#1에서 DecoratorClass를 정의한 뒤 #2, #3에서 @DecoratorClass로 적용하면 decorator_function을 사용한 것과 동일한 결과가 출력됩니다. 다만 클래스 형식의 데코레이터는 자주 쓰이지 않으며 일반적으로 함수 형식이 많이 사용됩니다. 클래스 형식도 가능하다는 점만 알아두면 충분합니다.
데코레이터의 기본 개념을 정리했으므로, 이번에는 실제 프로젝트에서 데코레이터가 어떻게 활용되는지 살펴봅니다.
데코레이터는 일반적으로 로그를 남기거나, 사용자의 로그인 상태를 확인해 로그인되어 있지 않으면 로그인 페이지로 리다이렉트(redirect)하는 용도로 자주 사용됩니다. 또한 프로그램의 성능 측정에도 많이 사용됩니다. 리눅스나 유닉스 서버 관리자는 스크립트의 실행 시간을 측정하기 위해 다음과 같은 date와 time 명령어를 자주 사용합니다.
$ date; time sleep 1; date
Thu Sep 8 00:13:28 JST 2016
real 0m1.007s
user 0m0.001s
sys 0m0.001s
Thu Sep 8 00:13:29 JST 2016데코레이터로 위와 비슷한 로깅 기능을 구현할 수 있습니다. 다음 예제를 살펴봅니다.
import datetime
import time
def my_logger(original_function):
import logging
filename = '{}.log'.format(original_function.__name__)
logging.basicConfig(handlers=[logging.FileHandler(filename, 'a', 'utf-8')],
level=logging.INFO)
def wrapper(*args, **kwargs):
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
logging.info('[{}] 실행결과 args - {}, kwargs - {}'.format(timestamp, args, kwargs))
return original_function(*args, **kwargs)
return wrapper
@my_logger
def display_info(name, age):
time.sleep(1)
print('display_info({}, {}) 함수가 실행됐습니다.'.format(name, age))위 코드를 실행하면 터미널에 다음과 같이 출력되며, decorator.py가 저장된 디렉터리에 display_info.log 로그 파일이 생성됩니다. 프로그램을 실행해 봅니다.
$ python decorator.py
display_info(John, 25) 함수가 실행됐습니다.로그 파일안에는 다음과 같은 정보가 기록된 것을 볼 수 있습니다.
INFO:root:[2021-10-03 21:13] 실행결과 args - ('John', 25), kwargs - {}이번에는 데코레이터를 하나 더 추가해 프로그램의 실행 시간을 측정해 봅니다.
import datetime
import time
def my_logger(original_function):
import logging
filename = '{}.log'.format(original_function.__name__)
logging.basicConfig(handlers=[logging.FileHandler(filename, 'a', 'utf-8')],
level=logging.INFO)
def wrapper(*args, **kwargs):
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
logging.info(
'[{}] 실행결과 args - {}, kwargs - {}'.format(timestamp, args, kwargs))
return original_function(*args, **kwargs)
return wrapper
def my_timer(original_function): # 1
import time
def wrapper(*args, **kwargs):
t1 = time.time()
result = original_function(*args, **kwargs)
t2 = time.time() - t1
print('{} 함수가 실행된 총 시간: {} 초'.format(original_function.__name__, t2))
return result
return wrapper
@my_timer # 2
def display_info(name, age):
time.sleep(1)
print('display_info({}, {}) 함수가 실행됐습니다.'.format(name, age))
display_info('John', 25)#1에서 my_timer라는 새로운 데코레이터를 정의하고, #2에서 display_info 함수에 적용했습니다. 프로그램을 실행합니다.
$ python decorator.py
display_info(John, 25) 함수가 실행됐습니다.
display_info 함수가 실행된 총 시간: 1.00157594681 초1초가 걸렸습니다.
이번에는 my_logger와 my_timer 두 데코레이터를 동시에 사용해 봅니다.
import datetime
import time
def my_logger(original_function):
import logging
filename = '{}.log'.format(original_function.__name__)
logging.basicConfig(handlers=[logging.FileHandler(filename, 'a', 'utf-8')],
level=logging.INFO)
def wrapper(*args, **kwargs):
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
logging.info(
'[{}] 실행결과 args - {}, kwargs - {}'.format(timestamp, args, kwargs))
return original_function(*args, **kwargs)
return wrapper
def my_timer(original_function):
import time
def wrapper(*args, **kwargs):
t1 = time.time()
result = original_function(*args, **kwargs)
t2 = time.time() - t1
print('{} 함수가 실행된 총 시간: {} 초'.format(original_function.__name__, t2))
return result
return wrapper
@my_logger # 1
@my_timer # 2
def display_info(name, age):
time.sleep(1)
print('display_info({}, {}) 함수가 실행됐습니다.'.format(name, age))
display_info('John', 25)$ python decorator.py
display_info(John, 25) 함수가 실행됐습니다.
display_info 함수가 실행된 총 시간: 1.00419592857 초INFO:root:[2021-10-03 21:13] 실행결과 args - ('John', 25), kwargs - {}#1, #2에 데코레이터 2개를 함께 사용했습니다. 터미널의 실행 결과는 이전과 동일하게 출력되었습니다. 그런데 기존 로그 파일에는 아무것도 기록되지 않았으며, 대신 wrapper.log라는 이름의 로그 파일이 새로 생성되어 다음과 같은 로그가 남았습니다.
INFO:root:[2021-10-03 21:17] 실행결과 args - ('John', 25), kwargs - {}왜 이런 결과가 나오는지 살펴보기 전에, 이번에는 데코레이터의 순서를 바꿔 봅니다.
from functools import wraps
import datetime
import time
def my_logger(original_function):
import logging
filename = '{}.log'.format(original_function.__name__)
logging.basicConfig(handlers=[logging.FileHandler(filename, 'a', 'utf-8')],
level=logging.INFO)
@wraps(original_function) # 1
def wrapper(*args, **kwargs):
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
logging.info(
'[{}] 실행결과 args - {}, kwargs - {}'.format(timestamp, args, kwargs))
return original_function(*args, **kwargs)
return wrapper
def my_timer(original_function):
import time
@wraps(original_function) # 2
def wrapper(*args, **kwargs):
t1 = time.time()
result = original_function(*args, **kwargs)
t2 = time.time() - t1
print('{} 함수가 실행된 총 시간: {} 초'.format(original_function.__name__, t2))
return result
return wrapper
@my_timer
@my_logger
def display_info(name, age):
time.sleep(1)
print('display_info({}, {}) 함수가 실행됐습니다.'.format(name, age))
display_info('Jimmy', 30) # 3INFO:root:[2021-10-03 21:13] 실행결과 args - ('John', 25), kwargs - {}
INFO:root:[2021-10-03 21:20] 실행결과 args - ('John', 25), kwargs - {}이번에는 로그 파일에 정상적으로 기록되었습니다. 다만 터미널에 출력된 결과에 이상한 점이 보입니다.
$ python decorator.py
display_info(John, 25) 함수가 실행됐습니다.
wrapper 함수가 실행된 총 시간: 1.0019299984 초display_info가 아닌 wrapper가 출력되었습니다.
원인은 단순합니다. 복수의 데코레이터를 스택해 사용하면 아래쪽 데코레이터부터 실행됩니다. 위 코드에서는 #1의 my_logger가 먼저 실행된 뒤 그 결과인 wrapper 함수가 #2의 my_timer에 인자로 전달됩니다. 따라서 my_timer 입장에서는 original_function이 사실상 wrapper 함수가 됩니다.
이런 현상을 방지하기 위해 제공되는 것이 functools 모듈의 wraps 데코레이터입니다. 다음과 같이 사용합니다.
from functools import wraps
import datetime
import time
def my_logger(original_function):
import logging
filename = '{}.log'.format(original_function.__name__)
logging.basicConfig(handlers=[logging.FileHandler(filename, 'a', 'utf-8')],
level=logging.INFO)
@wraps(original_function) # 1
def wrapper(*args, **kwargs):
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
logging.info(
'[{}] 실행결과 args - {}, kwargs - {}'.format(timestamp, args, kwargs))
return original_function(*args, **kwargs)
return wrapper
def my_timer(original_function):
import time
@wraps(original_function) # 2
def wrapper(*args, **kwargs):
t1 = time.time()
result = original_function(*args, **kwargs)
t2 = time.time() - t1
print('{} 함수가 실행된 총 시간: {} 초'.format(original_function.__name__, t2))
return result
return wrapper
@my_timer
@my_logger
def display_info(name, age):
time.sleep(1)
print('display_info({}, {}) 함수가 실행됐습니다.'.format(name, age))
display_info('Jimmy', 30) # 3#1, #2에서 wraps 데코레이터로 두 개의 wrapper 함수를 데코레이팅했고, 동작 검증을 위해 #3의 이름과 나이를 변경했습니다.
$ python decorator.py
display_info(Jimmy, 30) 함수가 실행됐습니다.
display_info 함수가 실행된 총 시간: 1.013833999633789 초INFO:root:[2021-10-03 21:13] 실행결과 args - ('John', 25), kwargs - {}
INFO:root:[2021-10-03 21:20] 실행결과 args - ('John', 25), kwargs - {}
INFO:root:[2021-10-03 21:27] 실행결과 args - ('Jimmy', 30), kwargs - {}의도한 결과가 터미널과 로그 파일에 모두 정상적으로 출력되었습니다.
여기까지가 데코레이터의 기본 사용법입니다. 개념을 익혔더라도 실제 프로젝트에서 활용해 보지 않으면 금세 잊히므로, 연습을 거듭해 직접 적용해 보길 권합니다.
다음 글에서는 파이썬의 제너레이터(Generator)를 다룹니다.