Corey Schafer의 Python Tutorial: Decorators를 보고 정리합니다.
- First-Class Functions
- Closures
- Decorators
def outer_function(msg):
def inner_function():
print(msg)
return inner_function
데코레이터는 Closure에서 했던 것과 매우 비슷하다. Decorator는 함수가 다른 함수를 argument로 받아 functionality를 추가하고 다른 함수(arugment로 받은)를 return하는 것이다.
def decorator_function(message):
def wrapper_function():
print(message)
return wrapper_function
hi_func = outer_fucntion('Hi')
bye_func = outer_function('Bye')
hi_func()
>> Hi
bye_func()
>> Bye
위의 코드는 recap의 closure를 데코레이터처럼 바꾼 것이다. 이것도 decorator_function은 실행 준비가 된 warpper_function을 return하고 wapper_function이 실행되면 message를 print한다.
만약 message를 argument로 받는게 아니라 함수를 arugment로 받고, 받은 message를 print하는게 아니라 받은 함수를 실행하면 어떻게 될까? 이것이 바로 decorator가 하는 일이다.
위에서 말한 것처럼 바꿔보자.
def decorator_function(original_function):
def wrapper_function():
return original_function()
return wrapper_function
def display():
print("display function ran")
decorated_display = decorator_function(display)
decorated_display()
>> display function ran
decorated_display
는 display
함수를 argument로 받아서 실행될 준비가된 warpper_function
을 return한다.
그래서 decorated_display()
를 하면 wrapper_function
이 실행되고 wrpper_function
은 original_function
(display
function)을 실행한다.
decorator는 wrpper안에 functionality를 추가함으로써 기존에 있는 function(original_function
)에 functionality들을 추가할 수 있다.
아래와 같이 display
함수를 일절 변경하지 않더라도 wrapper에 내가 원하는 코드를 넣어주면 된다.
def decorator_function(orignal_function):
def wrapper_function():
print('wrapper executed this before {}'.format(original_function.__name__))
return original_function()
return warpper_function
@decorator_function
def display():
print('display function ran')
display()
>> wrapper executed this before display
>> display function ran
더이상 display = decorator_function(display)
과 같이 표현하지 않아도, 함수 위에 @decorator를 붙이면 같은 의미를 지닌다.
그렇기 때문에 바로 display()
로 함수를 실행해주면 wapper가 실행되는 것이다.
만약 decorated function에 arguments가 온다면 그에 맞는 새로운 decorator함수를 또 만들어줘야하는걸까?
def display_info(name, age):
print('display_info ran with argumets ({}, {})'.format(name, age))
display_info('John', 25)
>> display_info ran with argumets (John, 25)
만약 이 display_info
함수에 위에서 사용했던 decorator를 적용해준다면 다음과 같은 에러가 발생한다.
@decorator_function
def display_info(name, age):
print('display_info ran with arguments ({}, {})'.format(name, age))
display_info('John', 25)
>> TypeError: wrapper_function() takes 0 positional arguments but 2 were given
만들어준 decorator_function
을 더 다양한 상황에 적용시키기 위해서는 wrapper_function
에 가변적인 arguemts(*args
, **kargs
)를 추가해준다.
def decorator_function(orignal_function):
def wrapper_function(*args, **kwargs):
print('wrapper executed this before {}'.format(original_function.__name__))
return original_function(*args, **kwargs)
return wrapper_function
이렇게 해주면 arguments를 가진 함수에도 decorator가 잘 적용되는 것을 볼 수 있다.
@decorator_function
def display_info(name, age):
print('display_info ran with argumets ({}, {})'.format(name, age))
display_info('John', 25)
>> wrapper executed this before display_info
>> display_info ran with argumets (John, 25)
def my_logger(orig_func):
import logging
logging.basicConfig(filename='{}.log'.format(orig_func.__name__), level=logging.INFO)
def wrapper(*args, **kwargs):
logging.info(
'Ran with args: {}, and kwargs: {}'.format(args, kwargs))
return orig_func(*args, **kwargs)
return wrapper
@my_logger
def display_info(name, age):
print('display_info ran with arguments ({}, {})'.format(name, age))
display_info('John', 25)
>> display_info ran with arguments (John, 25)
# display_info.log
INFO:root:Ran with args: ('John', 25), and kwargs: {}
display_info
를 위와 같이 실행하면 wrapper
에서 로깅을 실행하고 display_info('John', 25)
를 return함으로써 print가 실행된다.
만약 display_info('Hank', 36)
을 한번 더 실행한다면 다음과 같이 Hank에 대한 정보가 로깅에 추가되고 display_info ran with arguments (Hank, 36)
이 콘솔에 print될 것이다.
# display_info.log
INFO:root:Ran with args: ('John', 25), and kwargs: {}
INFO:root:Ran with args: ('Hank', 36), and kwargs: {}
이제 logging 기능을 원하는 모든 함수에 logging decorator를 붙일 수 있다.
def my_timer(orig_func):
import time
def wrapper(*args, **kwargs):
t1 = time.time()
result = orig_func(*args, **kwargs)
t2 = time.time() - t1
print('{} ran in: {} sec'.format(orig_func.__name__, t2))
return result
return wrapper
import time
@my_timer
def display_info(name, age):
time.sleep(1)
print('display_info ran with arguments ({}, {})'.format(name, age))
display_info('Hank', 36)
>> display_info ran with arguments (Hank, 36)
>> display_info ran in: 1.001725434673563 sec
위에서 선언한 my_logger
와 my_timer
데코레이터를 한 함수에 모두 적용하려면 어떻게 해야할까?한번 decorator를 두개 겹쳐보자.
@my_timer
@my_logger
def display_info(name, age):
time.sleep(1)
print('display_info ran with arguments ({}, {})'.format(name, age))
display_info('Hank', 36)
>> display_info ran with arguments (Hank, 36)
>> wrapper ran in: 1.00383674745643736434 sec
하지만 원하지 않은 결과가 나와버렸다. print된 실행결과를 보면 display_info
의 실행시간을 알고 싶었는데 wrapper
의 실행시간이 나와버린 것이다(logging은 잘 실행되었다.)
만약 두 데코레이터의 순서를 바꿔써주면 문제가 해결될까? @my_logger
, @my_timer
순으로 적어도 원하는 결과를 얻지 못한다. 이번에는 timer는 제대로 작동하지만 display_info의 log파일이 아닌 wrapper의 log파일이 생성되기 때문이다.
이러한 결과가 나타나는 이유는, 위의 코드와 같이 decorator를 겹쳐 써주는 것이 다음과 같은 의미를 갖기 때문이다.
display_info = my_timer(my_logger(display_info))
@my_logger
만 있을때display_info = my_logger(display_info)
와 같고 그 위에@my_timer
를 또 연결하면 이전의 결과를 한번 더 감싸게되므로
먼저 실행되는 my_logger(display_info)
가 wapper
그 자체이고 그럼 my_timer
는 orig_func
자리에 wrapper
를 받게 되므로 원래 의도는 display_info
를 실행하려고 했어도 wrapper
가 실행될 수 밖에 없는 것이다.
이 문제를 해결하기 위해서는 functool
모듈을 이용하여 wrapper
를 wraps
로 데코레이트해준다.
from functools import wraps
def my_logger(orig_func):
import logging
logging.basicConfig(filename='{}.log'.format(orig_func.__name__), level=logging.INFO)
@wraps(orig_func)
def wrapper(*args, **kwargs):
logging.info(
'Ran with args: {}, and kwargs: {}'.format(args, kwargs))
return orig_func(*args, **kwargs)
return wrapper
def my_timer(orig_func):
import time
@wraps(orig_func)
def wrapper(*args, **kwargs):
t1 = time.time()
result = orig_func(*args, **kwargs)
t2 = time.time() - t1
print('{} ran in: {} sec'.format(orig_func.__name__, t2))
return result
return wrapper
이렇게 처리해주면 my_logger(display_info)
가 display_info
가 되기 때문에 decorator를 두개 겹쳐썼을때 display_info
가 제대로 my_timer
까지 들어갈 수 있게 되는 것이다.
display_info('Hank', 36)
>> display_info ran in: 1.0004765434565321 sec
>> display_info ran with arguments (Hank, 36)