Decorators

rang-dev·2020년 6월 18일
0

Corey Schafer의 Python Tutorial: Decorators를 보고 정리합니다.

  1. First-Class Functions
  2. Closures
  3. Decorators

Recap: Clousre

def outer_function(msg):
    def inner_function():
    	print(msg)
    return inner_function

Decorators

데코레이터는 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_displaydisplay함수를 argument로 받아서 실행될 준비가된 warpper_function을 return한다.

그래서 decorated_display()를 하면 wrapper_function이 실행되고 wrpper_functionoriginal_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)

Logging

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를 붙일 수 있다.

Timming

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

Chain Decorators

위에서 선언한 my_loggermy_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_timerorig_func자리에 wrapper를 받게 되므로 원래 의도는 display_info를 실행하려고 했어도 wrapper가 실행될 수 밖에 없는 것이다.

이 문제를 해결하기 위해서는 functool 모듈을 이용하여 wrapperwraps로 데코레이트해준다.

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)
profile
지금 있는 곳에서, 내가 가진 것으로, 할 수 있는 일을 하기 🐢

0개의 댓글