Decorator - (2) 클래스 만들기

Decorator - (1) 함수 만들기 에서 Decorator를 함수로 만들어봤었다. 이번 시간엔 Decorator를 클래스로 만들어 볼 것인데, 먼저 그 전에 다음을 알아야 한다.

class HelloWorld():

    def __init_(self):
        pass

간단한 클래스이다. 이 클래스를 함수처럼 실행시켜 버리면 어떻게 될까?

helloworld = HelloWorld()  
helloworld()  

이렇게 실행하면

TypeError: 'HelloWorld' object is not callable  

물론 오류가 뜰 것이다. 왜냐하면 HelloWorld는 클래스이므로 함수처럼 호출할 수 없기 때문이다.
하지만 이 클래스를 함수처럼 호출할 수 있는 방법이 있다.

class HelloWorld():

    def __init_(self):
        pass

    def __call__(self):
        print("hello world")

위와 같이 __call__ 함수를 클래스에 추가해준다. __call__ 함수는 이 클래스의 객체가 함수처럼 호출되면 실행되는 함수이다. 아까의 코드를 다시 실행해보면 'hello world'가 정상적으로 출력될 것이다.
이와 같은 클래스의 기능을 이용하여 Decorator 클래스를 만들어 볼 것이다. 어떤 함수의 실행시간을 측정해주는 Decorator 클래스를 만들어보자.

import time

class Timer():

    def __init__(self, function):
        self.function = function

    def __call__(self, *args, **kwargs):
        start_time = time.time()
        result = self.function(*args, **kwargs)
        end_time = time.time()
        print("실행시간은 {time}초입니다.".format(time=end_time-start_time))
        return result

쉽게 말해서 여기서 __call__(self) 함수가 Decorator 함수에서의 wrapper 함수의 역할을 한다고 생각하면 된다. wrapper 함수에서처럼 Decorator 클래스가 적용될 함수의 parameter를 *args, **kwargs 가변인자로 받는다.
이제 이 Decorator 클래스를 특정 함수에 적용해보자.

@Timer
def print_hello(name):  
    print("hello, "+ name)

print_hello('python')  

특정함수 위에다가 Decorator 클래스를 선언해주면, 이 함수가 실행될 때 자동으로 Timer의 객체가 생성되고 __init__ 함수의 parameter로 이 함수 자신이 들어가게 된다. 그리고 객체가 함수처럼 호출됐으므로 아까와 같이 __call__ 함수가 실행되어 최종 결과를 출력하는 것이다. 나는 print_hello() 라는 함수만 실행했을 뿐이지만 사실 그 속에는 이렇게도 많은 일들이 일어나고 있다.
이 예시만 보면 굳이 Decorator 클래스를 써야 되나 라는 의문이 든다. Decorator 함수를 쓰면 더 간단한 게 사실이기 때문이다. 하지만 이렇게 클래스로 구현해놓으면 Decorator를 좀 더 구조화하기 쉬어진다.
예시를 보며 확인해보자.

class Tagify():

    def __init__(self, function):
        self.function = function

    def __call__(self, *args, **kwargs):
        tagified_p = self.tagify('p', self.function(*args, **kwargs))
        tagified_b = self.tagify('b', tagified_p)
        tagified_i = self.tagify('i', tagified_b)
        return tagified_i

    def tagify(self, tag, text):
        return "<{tag}>{text}</{tag}>".format(tag=tag, text=text)


@Tagify
def set_text(text):  
    return text

set_text('python') # <i><b><p>python</p></b></i>  

문자열을 i, b, p 태그로 감싸주는 Decorator 클래스이다. 태그를 감싸주는 작업이 계속 반복되므로 이 작업을 tagify라는 함수로 따로 빼놓은 것을 볼 수 있다. 지금도 그렇게 복잡한 Decorator은 아니지만 Decorator가 복잡해지면 복잡해질수록 클래스로 구조화해 놓는 것이 빛을 발한다.