파이썬 특수 문법(데코레이터, 이터레이터등) - . 데코레이터 (Decorator)

18. 데코레이터 (Decorator)

혹시 다음과 같이 @ 가 사용된 파이썬 코드를 본 적이 있으신지?

@decorator_func
def function():
    print ("what is decorator?")

위 코드에서 @decorator_func 부분이 데코레이터임

아니 이건 또 왜 이렇게 특이해!!!

  • 그래도 알아둘만 합니다. 또 가끔 이와 같은 코드를 볼 때 이해할 수 있어야 하기 때문에... 왜 사용하는지 알아보자.
In [57]:
# 다음 함수를 함 보자
def logger_login():
     print ("Dave login")

logger_login()
Dave login
In [62]:
# 시간을 앞뒤로 추가하고 싶다.
# 이렇게 넣으면 됩니다.
import datetime

def logger_login():
    print (datetime.datetime.now())
    print ("Dave login")
    print (datetime.datetime.now())

logger_login()
2018-03-10 14:42:54.984832
Dave login
2018-03-10 14:42:54.985410
In [63]:
# 아차 다른 비슷한 함수도 다 넣으려면.... 이걸 좀 깔끔하게...
def logger_login_david():
     print ("David login")

def logger_login_anthony():
     print ("Anthony login")

def logger_login_tina():
     print ("Tina login")

직접 각 함수에 기능을 앞뒤로 코드로 넣어도 되긴 되지 않을까?

  • 여러 함수에 동일한 기능을 @데코레이터 하나로 간편하게 추가할 수 있고,
  • 예를 들어, 파라미터가 있는 함수에 파라미터의 유효성 검사가 필요할 때
    • 파라미터가 있는 함수가 있을 때마다, 유효성 검사 코드를 넣기가 불편!
    • 만약 유효성 검사 코드 수정이 필요하다면 관련 함수를 모두 수정해야 하므로 매우 불편

18.1. 데코레이터 작성법

In [67]:
# 데코레이터 작성하기
def datetime_decorator(func):           # <--- datetime_decorator 는 데코레이터 이름, func 가 이 함수 안에 넣을 함수가 됨
    def wrapper():                      # <--- 호출할 함수를 감싸는 함수
        print ('time ' + str(datetime.datetime.now())) # <--- 함수 앞에서 실행할 내용
        func()                          # <--- 함수  
        print (datetime.datetime.now()) # <--- 함수 뒤에서 실행할 내용
    return wrapper                      # <--- closure 함수로 만든다.
In [69]:
# 데코레이터 적용하기
@datetime_decorator    # @데코레이터
def logger_login_david():
     print ("David login")

logger_login_david()
time 2018-03-10 14:47:11.827412
David login
2018-03-10 14:47:11.827933
In [70]:
@datetime_decorator    # @데코레이터
def logger_login_anthony():
     print ("Anthony login")

logger_login_anthony()
time 2018-03-10 14:47:12.395415
Anthony login
2018-03-10 14:47:12.395860
In [71]:
@datetime_decorator    # @데코레이터
def logger_login_tina():
     print ("Tina login")

logger_login_tina()
time 2018-03-10 14:47:15.394094
Tina login
2018-03-10 14:47:15.394653

Nested function, Closure function 과 함께 데코레이터를 풀어서 작성해보자

In [73]:
# decorator 함수 정의
def outer_func(function):
    def inner_func():
        print('decoration added')
        function()
    return inner_func

# decorating할 함수
def log_func():
    print('logging')
In [74]:
# 본래 함수
log_func()
logging
In [75]:
# log_func 함수에 inner_func 함수의 기능을 추가한 decorated_func 함수
decorated_func = outer_func(log_func)
decorated_func()  # <--- 결과는 데코레이터를 사용할 때와 동일함
decoration added
logging

이것을 한번에 데코레이터로 작성하면!

In [76]:
@outer_func
def log_func():
    print('logging')

log_func()
decoration added
logging
초간단 연습1
1. 위 코드에서 Nested function은?
3. 위 코드에서 Closure function은?

18.2. 파라미터가 있는 함수에 Decorator 적용하기

  • 중첩함수에 꾸미고자 하는 함수와 동일하게 파라미터를 가져가면 됨
In [80]:
# 데코레이터
def outer_func(function):
    def inner_func(digit1, digit2):
        if digit2 == 0:                       # <--- 유효성 검사의 예
            print('cannot be divided with zero')
            return 
        return function(digit1, digit2)
    return inner_func
In [81]:
# 실제 함수
def divide(digit1, digit2):
    return digit1 / digit2
In [82]:
# 데코레이터 사용하기 (유효성 검사)
@outer_func
def divide(digit1, digit2):
    return digit1 / digit2
In [83]:
print(divide(4, 2))
2.0
In [84]:
print(divide(9, 0))
cannot be divided with zero
None
In [88]:
def type_checker(function):
    def inner_func(digit1, digit2):
        if (type(digit1) != int) or (type(digit2) != int):
            print('only integer support')
            return 
        return function(digit1, digit2)
    return inner_func

@type_checker
def muliplexer(digit1, digit2):
    return digit1 * digit2

muliplexer(1.1, 2)
only integer support
초간단 연습1
type_checker 데코레이터 만들기 (인자 유효성 검사)
digit1, digit2 를 곱한 값을 출력하는 함수 만들기
type_checker 데코레이터로 digit1, digit2 가 정수가 아니면 'only integer support' 출력하고 끝냄
if (type(digit1) != int) or (type(digit2) != int):
In [68]:
# 데코레이터
def type_checker(function):
    def inner_func(digit1, digit2):
        if (type(digit1) != int) or (type(digit2) != int):                       # <--- 유효성 검사의 예
            print('cannot be divided with zero')
            return 
        return function(digit1, digit2)
    return inner_func

# 데코레이터 사용하기 (유효성 검사)
@type_checker
def divide(digit1, digit2):
    return digit1 * digit2

divide(0.1, 1)
cannot be divided with zero

18.3. 파라미터와 관계없이 모든 함수에 적용 가능한 Decorator 만들기

  • 파라미터는 어떤 형태이든 결국 (args, **kwargs) 로 표현 가능
  • 데코레이터의 내부함수 파라미터를 (args, **kwargs) 로 작성하면 어떤 함수이든 데코레이터 적용 가능
In [90]:
# 데코레이터 작성하기
def general_decorator(function):
    def wrapper(*args, **kwargs):
        print('function is decorated')
        return function(*args, **kwargs)
    return wrapper
In [91]:
# 데코레이터 적용하기
@general_decorator
def calc_square(digit):
    return digit * digit

@general_decorator
def calc_plus(digit1, digit2):
    return digit1 + digit2

@general_decorator
def calc_quad(digit1, digit2, digit3, digit4):
    return digit1 * digit2 * digit3 * digit4
In [92]:
# 함수 호출하기
print (calc_square(2))
print (calc_plus(2, 3))
print (calc_quad(2, 3, 4, 5))
function is decorated
4
function is decorated
5
function is decorated
120

18.4. 한 함수에 데코레이터 여러 개 지정하기

  • 함수에 여러 개의 데코레이터 지정 가능 (여러 줄로 @데코레이터를 써주면 됨)
  • 데코레이터를 나열한 순서대로 실행됨
In [93]:
# 여러 데코레이터 작성하기
def decorator1(function):
    def wrapper():
        print('decorator1')
        function()
    return wrapper
 
def decorator2(function):
    def wrapper():
        print('decorator2')
        function()
    return wrapper
In [94]:
# 여러 데코레이터를 함수에 한번에 적용하기
@decorator1
@decorator2
def hello():
    print('hello')
In [95]:
hello()
decorator1
decorator2
hello
In [100]:
def mark_bold(function):
    def wrapper(string):
        return '<b>' + function(string) + '</b>'
    return wrapper

def mark_italic(function):
    def wrapper(string):
        return '<i>' + function(string) + '</i>'
    return wrapper


@mark_bold
def contents(string):
    return string

@mark_italic
def contents2(string):
    return string

@mark_bold
@mark_italic
def contents3(string):
    return string

print (contents('안녕'))
print (contents2('안녕'))
print (contents3('안녕'))
<b>안녕</b>
<i>안녕</i>
<b><i>안녕</i></b>
도전 과제
다음 그림에 있는 HTML 웹페이지 태그를 붙여주는 데코레이터 만들기
해당 데코레이터를 사용해서 안녕하세요 출력해보기
In [80]:
def mark_bold(function):
    def wrapper(*args, **kwargs):
        return '<b>' + function(*args, **kwargs) + '</b>'
    return wrapper

def mark_italic(function):
    def wrapper(*args, **kwargs):
        return '<i>' + function(*args, **kwargs) + '</i>'
    return wrapper

@mark_bold
@mark_italic
def add_html(string):
    return string

print (add_html('안녕하세요'))
<b><i>안녕하세요</i></b>
In [101]:
%%html
<b>안녕</b>
<i>안녕</i>
<b><i>안녕</i></b>
안녕 안녕 안녕

18.5. Method Decorator

  • 클래스의 method에도 데코레이터 적용 가능
    • 클래스 method는 첫 파라미터가 self 이므로 이 부분을 데코레이터 작성시에 포함시켜야 함
In [102]:
# 데코레이터 작성하기 (for method)
def h1_tag(function):
    def func_wrapper(self, *args, **kwargs):            # <--- self 를 무조건 첫 파라미터로 넣어야 메서드에 적용가능
        return "<h1>{0}</h1>".format(function(self, *args, **kwargs))  # <--- function 함수에도 self 를 넣어야 함
    return func_wrapper
In [103]:
# 클래스 선언시 메서드에 데코레이터 적용하기
class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @h1_tag
    def get_name(self):
        return self.first_name + ' ' + self.last_name
In [104]:
# 데코레이터 적용 확인해보기
davelee = Person('Lee', 'Dave')
print(davelee.get_name())
<h1>Lee Dave</h1>
복습1 (format 함수 복습하기)
다음 코드 실행 결과 예상하고, 확인하기
In [105]:
print('{} {}'.format(10, 100))
10 100
In [106]:
print('{0} {2} {0} {1}'.format(10, 100, 20))
10 20 10 100
In [107]:
print('{1} {0}'.format(10, 100))
100 10
In [108]:
print('{aa} {bb}'.format(aa = 'aaaa', bb = 'cccc'))
aaaa cccc

18.6. 파라미터가 있는 Decorator 만들기 (심화)

  • decorator에 파라미터를 추가 가능
In [190]:
# 중첩 함수의 하나 더 깊게 두어 생성
def decorator1(num):
    def outer_wrapper(function):
        def innter_wrapper(*args, **kwargs):
            print('decorator1 {}'.format(num))
            return function(*args, **kwargs)
        return innter_wrapper
    return outer_wrapper
In [197]:
# 위와 같이 작성하면, 다음과 같이 호출할 수 있다.
print_hello = decorator1(1)(print_hello)
print_hello()
decorator1 1
decorator1 1
hello
In [196]:
# 이를 데코레이터로 표현하면 다음과 같다.
@decorator1(1)
def print_hello():
    print('hello')
In [195]:
print_hello()
decorator1 1
hello
In [200]:
# 이를 데코레이터로 표현하면 다음과 같다.(이렇게 써도 됨)
@decorator1(num=2)
def print_hello():
    print('hello')
In [201]:
print_hello()
decorator1 2
hello
도전 과제
다음 그림에 있는 HTML 웹페이지 태그와 같이 태그 이름을 넣으면 HTML 문법에 맞게 출력해주는 데코레이터를 만들기
해당 데코레이터를 사용해서 b, i, h1, h2, h3, h4, h5, h6, center 태그를 리스트로 넣어서 안녕하세요 출력해보기
In [81]:
def mark_html(tag):
    def outer_wrapper(function):
        def innter_wrapper(*args, **kwargs):
            return '<' + tag + '>' + function(*args, **kwargs) + '</' + tag + '>'
        return innter_wrapper
    return outer_wrapper

@mark_html('b')
def print_hello():
    return 'hello'

print(print_hello())
<b>hello</b>
In [ ]: