Decorators
이번엔 Python의 Decorators 에 대해 알아보자.
- Decorator(이하, 데코레이터)의 탄생 스토리를 알아보자.
- 데코레이터는 어떻게 작동하는 것일까.
- 데코레이터를 어디에 사용해야 할까.
- 데코레이터를 직접 만들어보자.
- 여러 개의 데코레이터를 적용해보자.
자, 바로 시작해보자.
데코레이터.
그것은 왜 탄생했을까.
데코레이터가 없던 시절의 데코레이터처럼 작동하는 코드를 봐보자.
(최초에는 classmethod와 staticmethod만 있었지만, 간단하게 def로 만들어서 예를 들어보자.)
# Before Decorators
def pretty_print(value):
print('Value is')
print(f'\t\t\t{value}')
print('-' * 50)
def add_one(num):
value = -1
if type(num) not in (int, float):
if type(num) is str:
if num.isdigit():
value = int(num)
else:
value = num
return value + 1
pretty_print(add_one(10))
pretty_print(add_one(14.0))
pretty_print(add_one('1923'))
pretty_print(add_one('일구이삼'))
pretty_print(add_one(list('01923')))
줄을 나누어 나름 프리티(?)하게 출력해주는 pretty_print 함수와
무엇인가 입력받아 숫자라면 1을 더하고 숫자가 아니라면, 0을 반환하는 add_one 함수가 있다.
그리고 add_one의 결과를 pretty_print 함수를 통해 프리티(?)하게 출력하려고 한다.
그리고 그 출력 결과는 ...
출력 결과 :
Value is
11
--------------------------------------------------
Value is
15.0
--------------------------------------------------
Value is
1924
--------------------------------------------------
Value is
0
--------------------------------------------------
Value is
0
--------------------------------------------------
문제 없이 훌륭하게 출력되었다.
위의 경우 처럼 코드작성을 할 때(데코레이터가 존재하지 않았을 때), 여러 문제가 발생하였다.
함수명이 매우매우 길어졌을 때,
def king_god_general_emperor_pretty_print(value):
print('Value is')
print(f'\t\t\t{value}')
print('-' * 50)
def king_god_general_emperor_any_add_one(num):
value = -1
if type(num) not in (int, float):
if type(num) is str:
if num.isdigit():
value = int(num)
else:
value = num
return value + 1
king_god_general_emperor_pretty_print(king_god_general_emperor_any_add_one(10))
king_god_general_emperor_pretty_print(king_god_general_emperor_any_add_one(14.0))
king_god_general_emperor_pretty_print(king_god_general_emperor_any_add_one('1923'))
king_god_general_emperor_pretty_print(king_god_general_emperor_any_add_one('일구이삼'))
king_god_general_emperor_pretty_print(king_god_general_emperor_any_add_one(list('01923')))
여러개의 함수를 덮어씌울 때, (여러개의 데코레이터를 사용할 때)
def king_god_general_emperor_pretty_print(value):
print('Value is')
print(f'\t\t\t{value}')
print('-' * 50)
def king_god_general_emperor_any_add_one(num):
value = -1
if type(num) not in (int, float):
if type(num) is str:
if num.isdigit():
value = int(num)
else:
value = num
return value + 1
def king_god_general_emperor_any_minus_one(num):
value = 1
if type(num) not in (int, float):
if type(num) is str:
if num.isdigit():
value = int(num)
else:
value = num
return value - 1
king_god_general_emperor_pretty_print(king_god_general_emperor_any_add_one(king_god_general_emperor_any_minus_one(10)))
king_god_general_emperor_pretty_print(king_god_general_emperor_any_add_one(king_god_general_emperor_any_minus_one(14.0)))
king_god_general_emperor_pretty_print(king_god_general_emperor_any_add_one(king_god_general_emperor_any_minus_one('1923')))
king_god_general_emperor_pretty_print(king_god_general_emperor_any_add_one(king_god_general_emperor_any_minus_one('일구이삼')))
king_god_general_emperor_pretty_print(king_god_general_emperor_any_add_one(king_god_general_emperor_any_minus_one(list('01923'))))
이런 해괴한 코드가 되어버릴 수 있다. 물론 예시처럼 함수명을 작성할 리는 없을 것이다. 아마도.
Python의 철학인 가독성 좋은 코드를 무시해버리는 결과가 나오게 되는 것이다.
그렇기 때문에 추가된 것이 바로 데코레이터인 것이다.
(물론 이런 간단한 이유만은 아닐 것이다.)
위 두 함수를 데코레이터를 이용해서 구현해보면,
# After Decorators
def pretty_print(func):
def wrapper(*args):
print('Value is')
print(f'\t\t\t{func(*args)}')
print('-' * 50)
return wrapper
@ pretty_print
def add_one(num):
value = -1
if type(num) not in (int, float):
if type(num) is str:
if num.isdigit():
value = int(num)
else:
value = num
return value + 1
add_one(10)
add_one(14.0)
add_one('1923')
add_one('일구이삼')
add_one(list('01923'))
출력 결과 :
Value is
11
--------------------------------------------------
Value is
15.0
--------------------------------------------------
Value is
1924
--------------------------------------------------
Value is
0
--------------------------------------------------
Value is
0
--------------------------------------------------
훨씬 간단해졌다.
함수 위에 @ 표시와 다른 함수의 이름만 적어두었는데, 작동되다니.
어떻게 작동하는 걸까.
이번엔 데코레이터가 어떻게 작동하는지 알아보자.
데코레이터가 없던 시절을 살펴보자.
그 시절의 @classmethod, @staticmethod를 선언하기 위해서는,
class C:
def meth (cls):
...
meth = classmethod(meth)
class C:
def meth ():
...
meth = staticmethod(meth)
이렇게 클래스의 맨 하단 혹은 클래스 선언 아래에 직접 선언해주었다.
위의 예시처럼 메서드 meth의 객체를 직접 classmethod, staticmethod 함수의 argument로 넘겨주었다.
그리고 반환 값(객체)을 다시 같은 meth에 덮어쓰기를 해버렸다.
(여기서는 값보다는 객체 또는 코드라고 보는 것이 이해하기 더 쉬울 것 같다.)
위의 함수들로 예시를 들어보자.
add_one 함수와 객체를 argument로 받는 pretty_print 함수를 선언하였다.
def pretty_print(func):
def wrapper(*args):
print('Value is')
print(f'\t\t\t{func(*args)}')
print('-' * 50)
return wrapper
def add_one(num):
value = -1
if type(num) not in (int, float):
if type(num) is str:
if num.isdigit():
value = int(num)
else:
value = num
return value + 1
add_one 함수를 pretty_print의 argument로 전달한다.
그리고 반환 객체를 다시 add_one 식별자에 저장한다.
add_one = pretty_print(add_one)
완성된 코드를 실행해보면,
def pretty_print(func):
def wrapper(*args):
print('Value is')
print(f'\t\t\t{func(*args)}')
print('-' * 50)
return wrapper
def add_one(num):
value = -1
if type(num) not in (int, float):
if type(num) is str:
if num.isdigit():
value = int(num)
else:
value = num
return value + 1
add_one = pretty_print(add_one)
add_one(10)
add_one(14.0)
add_one('1923')
add_one('일구이삼')
add_one(list('01923'))
출력 결과 :
Value is
11
--------------------------------------------------
Value is
15.0
--------------------------------------------------
Value is
1924
--------------------------------------------------
Value is
0
--------------------------------------------------
Value is
0
--------------------------------------------------
훌륭하게 작동했다.
반환된 add_one의 안을 들여다 보자면,
def add_one(num):
print('Value is')
print(f'\t\t\t{
# 여기에 원래 add_one의 코드가 들어간다.
value = -1
if type(num) not in (int, float):
if type(num) is str:
if num.isdigit():
value = int(num)
else:
value = num
value + 1
}')
print('-' * 50)
이렇게 작성하면, 당연히 오류가 난다.
슈도코드라고 생각하고 보도록 하자.
그럼 이 데코레이터를 어디에 사용하면 좋을까? 한번 생각해보자.
- argument, 오류 정보를 로그를 텍스트 파일에 기록하는 데코레이터
지금 생각나는 포인트는 이 정도가 있겠다.절대 구현하기 귀찮아서 1개만 하는 것은 아니다.
생각한 데코레이터의 특징이 무엇일까.
바로 굉장히 짧으면서 없어서는 안되며, 반복적으로 사용된다는 것이다.
이러한 특징을 가진 코드를 데코레이터로 만들어 둔다면, 개발 작업이 확실히 편하겠다.
자. 그럼 기다리고 기다리던 구현 시간이다.
argument, 오류 정보를 로그를 텍스트 파일에 기록하는 데코레이터,
# Logging Decorator
def record_log(func):
def wrapper(*args, **kwargs):
# result의 반환값은 Tuple(bool, Any)로 약속.
result = func(*args, **kwargs)
# 반환값 초기화
return_value = None
with open('./log.txt', 'a') as log_file:
# 0 번째 값이 False면 오류가 발생되었다는 것을 표현.
if result[0]:
# 1 번째 값은 sys.exc_info() 객체
log_file.write(f"ERROR OCCURRED CLASS : {str(result[1][0])}\n")
log_file.write(f"ERROR OCCURRED DESC : {str(result[1][0])}\n")
log_file.write(f"ERROR OCCURRED L_NUM : {str(result[1][0].tb_lineno)}\n")
else:
# 오류가 아니라면, 정상 반환값 할당
return_value = result[1]
# 아규먼트들 기록
log_file.write(f"ERROR OCCURRED ARGS : {str(args)}\n")
log_file.write(f"ERROR OCCURRED KARGS : {str(kwargs)}\n")
log_file.write('-' * 50)
# 반환값 반환
return return_value
return wrapper
애써 만든 데코레이터를 그냥 둘 수는 없다. 바로 실행해보자.
여기서는 add_one 함수에 record_log, pretty_print 2개의 데코레이터를 적용할 것이다.
여러개의 데코레이터를 적용할 때는, 가장 위의 데코레이터가 가장 바깥에 감싸진다(wrapping된다).
def deco1(func):
def wrapper():
print('deco1')
func()
print('deco1')
return wrapper
def deco2(func):
def wrapper():
print('deco2')
func()
print('deco2')
return wrapper
# deco2(deco1(func1()))
@deco2
@deco1
def func1():
print('func1')
func1()
출력 결과 :
deco2
deco1
func1
deco1
deco2
의미가 통하도록 감싸보았다.
(실제로 프린트된 순서와 감싸진(wrapping된) 순서는 상관 없다. 단지 데코레이터가 씌워지면 이런 모양일 것이라고만 생각하자.)
그렇다면 이렇게 데코레이터를 씌우면 되겠다.
import sys
def pretty_print(func):
def wrapper(*args):
print('Value is')
print(f'\t\t\t{func(*args)}')
print('-' * 50)
return wrapper
def record_log(func):
def wrapper(*args, **kwargs):
# result의 반환값은 Tuple(bool, Any)로 약속.
result = func(*args, **kwargs)
# 반환값 초기화
return_value = None
with open('./log.txt', 'a') as log_file:
# 0 번째 값이 False면 오류가 발생되었다는 것을 표현.
if result[0]:
# 1 번째 값은 sys.exc_info() 객체
log_file.write(f'ERROR OCCURRED CLASS : {str(result[1][0])}\n')
log_file.write(f'ERROR OCCURRED DESC : {str(result[1][0])}\n')
log_file.write(f'ERROR OCCURRED L_NUM : {str(result[1][0].tb_lineno)}\n')
else:
# 오류가 아니라면, 정상 반환값 할당
return_value = result[1]
# 아규먼트들 기록
log_file.write(f'ARGUMENTS : {str(args)}\n')
log_file.write(f'KEYWORD ARGUMENTS : {str(kwargs)}\n')
log_file.write('-' * 50)
# 반환값 반환
return return_value
return wrapper
@ pretty_print
@ record_log
def add_one(num):
value = -1
if type(num) not in (int, float):
if type(num) is str:
if num.isdigit():
value = int(num)
else:
value = num
return value + 1
add_one(10)
add_one(14.0)
add_one('1923')
add_one('일구이삼')
add_one(list('01923'))
출력 결과 :
Value is
11
--------------------------------------------------
Value is
15.0
--------------------------------------------------
Value is
1924
--------------------------------------------------
Value is
0
--------------------------------------------------
Value is
0
--------------------------------------------------
log.txt 내용 :
ARGUMENTS : (10,)
KEYWORD ARGUMENTS : {}
--------------------------------------------------
ARGUMENTS : (14.0,)
KEYWORD ARGUMENTS : {}
--------------------------------------------------
ARGUMENTS : ('1923',)
KEYWORD ARGUMENTS : {}
--------------------------------------------------
ARGUMENTS : ('일구이삼',)
KEYWORD ARGUMENTS : {}
--------------------------------------------------
ARGUMENTS : (['0', '1', '9', '2', '3'],)
KEYWORD ARGUMENTS : {}
--------------------------------------------------
아래는 raise를 통해 Exception을 일으키는 변형 add_one 함수이다.
@ pretty_print
@ record_log
def add_one(num):
try:
raise Exception('에러가 났어요!')
value = -1
if type(num) not in (int, float):
if type(num) is str:
if num.isdigit():
value = int(num)
else:
value = num
return True, value + 1
except:
return False, sys.exc_info()
add_one(10)
add_one(14.0)
add_one('1923')
add_one('일구이삼')
add_one(list('01923'))
출력 결과 :
Value is
None
--------------------------------------------------
Value is
None
--------------------------------------------------
Value is
None
--------------------------------------------------
Value is
None
--------------------------------------------------
Value is
None
--------------------------------------------------
log.txt 내용 :
ERROR OCCURRED CLASS : <class 'Exception'>
ERROR OCCURRED DESC : 에러가 났어요!
ERROR OCCURRED L_NUM : 199
ARGUMENTS : (10,)
KEYWORD ARGUMENTS : {}
--------------------------------------------------
ERROR OCCURRED CLASS : <class 'Exception'>
ERROR OCCURRED DESC : 에러가 났어요!
ERROR OCCURRED L_NUM : 199
ARGUMENTS : (14.0,)
KEYWORD ARGUMENTS : {}
--------------------------------------------------
ERROR OCCURRED CLASS : <class 'Exception'>
ERROR OCCURRED DESC : 에러가 났어요!
ERROR OCCURRED L_NUM : 199
ARGUMENTS : ('1923',)
KEYWORD ARGUMENTS : {}
--------------------------------------------------
ERROR OCCURRED CLASS : <class 'Exception'>
ERROR OCCURRED DESC : 에러가 났어요!
ERROR OCCURRED L_NUM : 199
ARGUMENTS : ('일구이삼',)
KEYWORD ARGUMENTS : {}
--------------------------------------------------
ERROR OCCURRED CLASS : <class 'Exception'>
ERROR OCCURRED DESC : 에러가 났어요!
ERROR OCCURRED L_NUM : 199
ARGUMENTS : (['0', '1', '9', '2', '3'],)
KEYWORD ARGUMENTS : {}
--------------------------------------------------
모두 훌륭하게 작동했다.
(텍스트 파일은 보기 편하게 실행 전 초기화 해주었다.)
길고 긴 데코레이터가 끝났다. 너무 두서없이 작성된 건 아닌가 걱정이 든다.
계속해서 읽고 복습하면서 추가하고 수정해야겠다.
데코레이터는 거의 써본 적이 전무했기 때문에 공부하는 데 계획했던 것 보다 더 많은 시간이 걸렸다.
또 알고 있었더라면 다른 프로젝트를 할 때 많은 도움이 되었을 것이 눈에 보여서, 조금 마음이 아팠다.
+ 면접 질문에서 데코레이터가 무엇인지 물어보았을 때, 잘 모르겠다고 대답한 기억이 있다.
나름 트라우마로 남아, 데코레이터는 꼭 포스팅해야겠다고 마음 먹었었다. 다음번엔 완벽하게 대답을 해주리라.
자, 또 다음 장으로 넘어가보자.
출처:
What’s New in Python 2.4 — Python 3.11.1 문서
https://docs.python.org/ko/3/whatsnew/2.4.html?highlight=decorators#pep-318-decorators-for-functions-and-methods
What’s New in Python 2.4
Author, A.M. Kuchling,. This article explains the new features in Python 2.4.1, released on March 30, 2005. Python 2.4 is a medium-sized release. It doesn’t introduce as many changes as the radical...
docs.python.org
PEP 318 - Decorators for Functions and Methods
https://peps.python.org/pep-0318/
PEP 318 – Decorators for Functions and Methods | peps.python.org
Guido asked for a volunteer to implement his preferred syntax, and Mark Russell stepped up and posted a patch to SF. This new syntax was available in 2.4a2. @dec2 @dec1 def func(arg1, arg2, ...): pass This is equivalent to: def func(arg1, arg2, ...): pass
peps.python.org
데코레이터의 여러가지 사용방법
PythonDecoratorLibrary - Python Wiki
https://wiki.python.org/moin/PythonDecoratorLibrary
PythonDecoratorLibrary - Python Wiki
This page is meant to be a central repository of decorator code pieces, whether useful or not <wink>. It is NOT a page to discuss decorator syntax! Feel free to add your suggestions. Please make sure example code conforms with PEP 8. Creating Well-Behaved
wiki.python.org