본문 바로가기
Back-end/Python

[문법] 데코레이터 - Decorator

by 허도치 2019. 12. 24.

  보통 함수가 정상적으로 실행되는지 확인하기위해 시작 로그와 종료 로그를 출력한다. 그러나, 로그를 출력하기위해 매번 함수의 시작과 끝에 로거를 작성하는 것은 매우 비효율적인 일이다. 이때 필요한 것이 바로 '데코레이터(Decorator)'이다.

 

  데코레이터는 함수를 한번 감싸주어 함수가 실행되기 전과 후를 컨트롤 할 수 있게 도와준다. 함수가 실행되기 전과 후에 로그를 출력, 함수가 실행되기 전 사용자 인증, 함수가 실행된 결과를 변조하는 등 다양한 방식으로 사용된다.

 

  Flask에서 Route를 설정하기 위해서는 아래 사진과 같이 @app.route를 함수를 생성할 때 위에 선언해주는데 이것이 바로 데코레이터이다. 데코레이터는 앞에 '@' 키워드를 붙여서 사용한다.

 

 

  함수를 실행할때 시작과 끝에 로그를 남겨주는 데코레이터 예제를 통해 데코레이터를 만드는 방법을 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def logger(func):
  
  func_name = func.__name__
 
  def wrapper(*args, **kwargs):
    '''
    함수의 시작과 끝에 로그를 출력하는 데코레이터.
    '''
    print(f'[{func_name}] start')
 
    res = func(*args, **kwargs)
 
    print(f'[{func_name}] end')
    return res
  return wrapper
cs

  - ln 1: func를 매개변수로 받고 있는데, 데코레이터를 사용하는 함수가 이 변수로 전달됨.

  - ln 3: 데코레이터를 사용하는 메인 함수의 이름을 저장.

  - ln 5: 데코레이터의 바디 함수로써 데코레이터를 사용하는 메인 함수에서 전달한 인자를 매개변수로 받아서 사용할 수 있음. 주로 wrapper라고 네이밍하여 사용됨. 전처리된 데이터를 추가로 전달할 수도 있음.

  - ln 6~8: 함수의 Description.

  - ln 9, 13: 함수가 실행되기 전과 후에 로그를 출력.

  - ln 11: 데코레이터를 사용하는 함수를 실행하고 결과값을 저장.

  - ln 14: 함수가 실행된 결과를 반환.

  - ln 15: 데코레이터의 바디 함수를 반환.

 

 

  이번에는 위에서 생성한 logger 데코레이터를 사용하는 일반 함수를 생성해보자.

1
2
3
4
5
6
@logger
def run():
  '''
  데코레이터를 사용해보자.
  '''
  print('데코레이터 테스트입니다.')
cs

  - ln 1: logger 데코레이터를 사용. 매개변수를 전달하는 함수형 데코레이터가 아니므로 괄호는 생략.

  - ln 2~6: 일반 함수 생성.

 

 

  실행 결과를 확인해보자.

1
2
3
4
5
6
7
run()
 
''' 실행결과
[run] start
데코레이터 테스트입니다.
[run] end
'''
cs

  - 이것으로 간단한 데코레이터를 만들어보았다. 어떤 함수에서든 logger 데코레이터를 사용하면 함수의 실행과 종료에 로그를 출력할 수 있다. 

 

  그럼 맨 처음 소개했던 Flask의 @app.route처럼 인자값을 전달하는 함수형 데코레이터는 어떻게 생성하는지 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def logger(log_level='INFO'):
  def decorator( func ):
    func_name = func.__name__
 
    def wrapper(*args, **kwargs):
      '''
      함수의 시작과 끝에 로그를 출력하는 데코레이터.
      '''
      print(f'[{func_name}][{log_level}] start')
 
      res = func(*args, **kwargs)
 
      print(f'[{func_name}][{log_level}] end')
      return res
    return wrapper
  return decorator
 
@logger(log_level="DEBUG")
def run():
  '''
  데코레이터를 사용해보자.
  '''
  print('데코레이터 테스트입니다.')
    
run()
 
''' 실행결과
[run][DEBUG] start
데코레이터 테스트입니다.
[run][DEBUG] end
'''
cs

  - ln 1~16: 처음 만들었던 logger 함수를 decorator 함수로 변경하고, 이 decorator 함수를 다른 함수로 한번 더 감싸줌.. 이 때 한번 더 감싸준 함수를 logger로 명명.

  - ln 18: 함수에 인자값을 전달. log_level="DEBUG"

  - ln 27~30: 실행결과에 데코레이터로 전달한 인자값이 출력되는 것이 확인.

 

 

  이것으로 데코레이터를 생성하는 방법을 알아보았다. 위와 같이 만들면 사실상 사용하는데에 있어서 특별한 문제는 없다. 그러나, 데코레이터를 사용하는 메인 함수의 정보를 확인( __name__, __doc__ 등 ) 할 때 문제가 발생한다. 위에서 생성한 run 함수의 정보를 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
print( run )
'''
<function logger.<locals>.decorator.<locals>.wrapper at 0x10dbafef0>
'''
 
print( run.__name__ )
'''
wrapper
'''
 
print( run.__doc__ )
'''
함수의 시작과 끝에 로그를 출력하는 데코레이터.      
'''
cs

  - ln 3: run 함수를 출력하였는데, logger 데코레이터 함수가 출력.

  - ln 8: run 함수의 이름을 출력하였는데, logger 데코레이터의 바디 함수의 이름이 출력.

  - ln 13: run 함수의 Description을 출력하였는데, logger 데코레이터의 바디 함수의 Description이 출력.

 

 

  run함수의 정보를 출력해보았다. 그러나, 결과값을 예상과 달리 데코레이터 함수의 정보들이 출력되는 것을 볼 수 있다. 일반적으로 사용하는데에 큰 문제는 없지만, 정확한 정보를 출력하기위해 이를 수정해보도록 하겠다.

1
2
3
4
5
6
7
8
9
10
11
from functools import wraps
 
def logger(log_level='INFO'):
  def decorator( func ):
 
    func_name = func.__name__
 
    @wraps( func )
    def wrapper(*args, **kwargs):
      '''
      중략
cs

  - ln 1: wraps는 update_wrapper()와 partial()를 사용하여 데코레이터를 사용하는 함수의 정보를 출력하도록 도와주는 데코레이터.

  - ln 8: wraps 데코레이터의 인자로 함수를 전달.

 

 

  위와 같이 소스를 수정하고 결과값을 출력해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
print( run )
'''
<function run at 0x10784bb90>
'''
 
print( run.__name__ )
'''
run
'''
 
print( run.__doc__ )
'''
데코레이터를 사용해보자.
'''
cs

  - 이제 정상적으로 logger 데코레이터를 사용하는 함수의 정보를 출력하는 것을 확인.

 

 

 

  이것으로 파이썬의 데코레이터를 알아보았다. 추가적으로 데코레이터는 여러개 사용할 수 있으며, 위에서부터 순차적으로 데코레이터가 적용된다. 이번 포스트에서는 소개하지 않았지만 Flask의 @app.route는 클래스로 만든 데코레이터인데 위에서 만든 데코레이터를 단순히 클래스로 한번 더 감싸주면 끝이다. 데코레이터는 파이썬을 사용하면 자주사용되는 기능이므로 반드시 익혀두는 것을 추천한다.

 

 

추가. Class를 이용한 Decorator

 

방법1. 일반적인 Class 데코레이터.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Logger(object):
  def __init__(self, func):
    self.func = func
 
  def __call__(self, *args, **kwargs):
    print('start')
    res = self.func(*args, **kwargs)
    print('end')
    return res
 
@Logger
def tester():
  print('run func')
cs

 

방법2. Parameter를 받는 데코레이터.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Logger(object):
  def __init__(self, *args, **kwargs):
    self._args = args
    self._kwargs = kwargs
 
  def __call__(self, func):
    @wraps( func )
    def wrapper(*args, **kwargs):
      print(f'{self._args} {self._kwargs}')
      print('start')
      res = func(*args, **kwargs)
      print('end')
      return res
    return wrapper
  
@Logger( 1, data=10 )
def tester():
  print('run func')
cs

 

방법3. 클래스의 정적 메소드를 이용한 체인 데코레이터.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Logger(object):
  @staticmethod
  def logging( log_level="INFO" ):
    def wrapper( func ):
      
      func_name = func.__name__
 
      @wraps( func )
      def decorator( *args, **kwargs ):
        print(f'[{func_name}][{log_level}] start')
        
        res = func( *args, **kwargs )
 
        print(f'[{func_name}][{log_level}] end')
        return res
      return decorator
    return wrapper
 
@Logger.logging(log_level="DEBUG")
def tester():
  print('run func')
cs

댓글