본문 바로가기

진리는어디에/Python

[Python] 클래스(class) #7 클래스 데코레이터

이번 포스트에서는 이전에 배운 callable object를 이용하여 클래스 형태의 데코레이터(decorator)를 만들어 보도록 하겠다.

목차

  1. 파이썬 클래스 소개
  2. instance vs static
  3. dict vs slots
  4. property의 활용
  5. special method
  6. callable object
  7. >> 클래스 데코레이터(class decorator)
  8. 상속
  9. 추상 클래스와 추상 메소드

Callable Object 와 Decorator

본격적인 설명에 앞서 이전 강좌를 못 본 분들도 있을 것이므로 callable object와 decorator에 대해 간단한 한줄 요약을 정리하고 넘어 가도록 하겠다. 자세한 사항은 각 링크된 포스트들을 살펴 보도록 한다(아무리 요약이 있더라도 꼭 살펴 보도록 하자).

  • callable object : 스페셜 메소드 __call__을 이용하여 객체를 함수처럼 사용 할 수 있도록 한 객체. 상태를 가지는 함수를 만들 수 있다. 자세한 사항은 [여기]를 참고
  • 데코레이터(decorator) : 기존 함수를 변경하지 않으면서 함수에 기능을 추가 할 수 있게 해주는 도구. 자세한 사항은 [여기]를 참고

이번 포스트에서는 callable object와 데코레이터를 결합하여 상태를 가질 수 있는 '클래스 데코레이터'를 만들어 보도록 하겠다.

클래스 데코레이터

우리는 앞에서 데코레이터에 대해 배운적이 있다. 데코레이터는 함수로 만들 수도 있지만 클래스로도 만드는 것이 가능하다. 이전 강의에서 다루었던 함수로 만들어진 데코레이터 예제를 먼저 살펴 보자.

# 아직 까진 함수로 만든 데코레이터
def add_emoticon(func) :

    def inner(*args, **kwargs) :
         print('= ̄ω ̄= ', end='')
         func(*args, **kwargs)
    
    return inner
    
@add_emoticon    # say_hi = add_emoticon(say_hi) 와 동일
def say_hi(name) :
    print(f'hi {name}!')
    
say_hi('Jason')

위 예제에서 우리가 @add_emoticon 데코레이터를 사용하면 인터프리터에 의해 say_hi = add_emoticon(say_hi)와 같은 코드로 바뀐다고 이야기 했었다.

여기서 우리가 주목 해야 할 부분은  "add_emoticon(say_hi)"는 클래스 객체 생성과 동일한 형태라는 것이다. 우리는 이것을 이용하여 함수 데코레이터를 클래스 데코레이터로 변경 할 것이다.

'''
def add_emoticon(func) :

    def inner(*args, **kwargs) :
         print('= ̄ω ̄= ', end='')
         func(*args, **kwargs)
    
    return inner
'''

class add_emoticon :            # 함수와 이름이 같다
    def __init__(self, func) :  # add_emoticon(say_hi) 처리
        self.func = func
    
@add_emoticon                   # say_hi = add_emoticon(say_hi) 와 동일
def say_hi(name) :
    print(f'hi {name}!')

say_hi('Jason')

클래스의 생성자(__init__)를 이용해 함수를 인자로 받는 add_emoticon(say_hi) 부분을 처리했다. 하지만 아직 위 예제는 정상적으로 동작하지 않는다. add_emoticon은 함수가 아니라 클래스 객체기 때문에 함수 처럼 호출 할 수 없다.

이제는 객체를 함수 처럼 호출 할 수 있도록 __call__ 스페셜 메소드를 적용해 보자. 하는 김에 단순히 name 만 처리하는 것이 아닌 이전에 배웠던 파라미터 패킹 / 언패킹을 이용해 가변 인자를 처리 할 수 있도록 수정 해보자.

class add_emoticon :
    def __init__(self, func) :             # add_emoticon(say_hi) 처리
        self.func = func
        
    def __call__(self, *args, **kwargs) :  # 실제 데코레이팅
        print('^_^ ', end='')
        return self.func(*args, **kwargs)
    
@add_emoticon                              # say_hi = add_emoticon 객체 생성
def say_hi(name) :
    print(f'hi {name}!')

say_hi('Jason')                            # say_hi.__call__('Jason') 과 동일
print(type(say_hi))                        # <class '__main__.add_emoticon'>

이렇게 __call__ 스페셜 메소드를 이용해 객체를 함수 처럼 호출 할 수 있도록하여 클래스를 데코레이터와 동일 하게 사용 할 수 있다. 실제 say_hi 객체의 타입을 출력하면 함수가 아니라 클래스라고 출력 되는 것을 확인 할 수 있다.

인자를 가진 클래스 데코레이터

지금 부터는 위 데코레이터를 확장하여 인자를 가지는 데코레이터를 만들어 보도록 하자. 아래는 새로운 이모티콘을 인자로 받을 수 있는 데코레이터 형태로 수정해 보았다.

# @add_emoticon         # say_hi = add_emoticon(say_hi)    
@add_emoticon('-_-')    # say_hi = add_emoticon('-_-')(say_hi)
def say_hi(name) :
    print(f'hi {name}!')

이전에 우리는 데코레이터에 인자를 지정하게 되면 say_hi = add_emocoticon('-_-')(say_hi) 와 같은 코드를 인터프리터가 생성한다는 것을 배웠었다.

위와 같은 형태라면 add_emoticon 클래스의 생성자는 함수를 넘겨 받는 것이 아니라 이모티콘을 넘겨 받게 된다. 그렇다면 다음과 같이 add_emoticon의 생성자가 이모티콘을 받아 저장 할 수 있도록수정 하도록 하자.

class add_emoticon :
    # def __init__(self, func) :
    #    self.func = func
    
    def __init__(self, emoticon) :         # 함수 대신 이모티콘을 인자로 받아 저장
        self.emoticon = emoticon
        
    def __call__(self, *args, **kwargs) :  # 실제 데코레이팅
        print('^_^ ', end='')
        return self.func(*args, **kwargs)

이제 add_emoticon 클래스가 생성자에서 이모티콘을 받도록 수정 되면서 오히려 함수를 넘겨 받는 부분이 없어졌다.  다시 add_emocoticon('-_-')(say_hi)를 상기해 보자. 인터프리터는는 객체 생성 이후에 바로 함수를 인자로 넘겨주는 호출을 하고 있다. 그럼 이제 __call__ 특수 메소드가 함수를 받을 수 있도록 수정 해보자.

그런데 이 부분에서 한번 생각 해봐야 할 것이 __call__ 메소드 호출의 결과는 say_hi 변수에 저장 되고, 이 say_hi를 호출 하게 되면 데코레이팅 된 결과를 얻을 수 있어야 한다는 것이다. 방법으로는 새로운 callable object를 만들어서 넘겨도 되고, 기존에 배운 함수객체를 만들어서 넘겨도 된다. 여기서는 이전 우리가 배웠던 함수 객체를 이용한 데코레이팅 기법을 기억해 데코레이팅이 적용 된 inner 함수를 리턴 하도록 한다.

class add_emoticon :
    # def __init__(self, func) :
    #    self.func = func
    
    def __init__(self, emoticon) :         # 함수 대신 이모티콘을 인자로 받아 저장
        self.emoticon = emoticon
        
    # def __call__(self, *args, **kwargs) :  # 실제 데코레이팅
    #    print('^_^ ', end='')
    #    return self.func(*args, **kwargs)
    
    def __call__(self, func) :
        
        def inner(*args, **kwargs) :
            print(self.emoticon, ' ', end='')
            return func(*args, **kwargs)
            
        return inner
        
@add_emoticon('-_-')    # say_hi = add_emoticon('-_-')(say_hi)
def say_hi(name) :
    print(f'hi {name}!')
    
say_hi('Jason')

상태를 가지는 데코레이터

callable object를 사용하면 함수 처럼 사용 할 수 있으면서도 상태를 저장 할 수 있다고 했다. 이제 위 예제를 다시 살짝 변형에 add_emoticon 데코레이터를 이용한 say_hi함수가 몇번 호출 되었는지 추적 할 수 있도록 해보자.

class add_emoticon :
    
    def __init__(self, emoticon) :         
        self.count = 0                       # 호출 회수 저장 필드
        self.emoticon = emoticon
    
    def __call__(self, func) :
        
        def inner(*args, **kwargs) :
            self.count += 1
            inner.count = self.count         # 함수 객체에서 바로 사용 할 수 있도록
            print(self.emoticon, ' ', end='')
            return func(*args, **kwargs)
            
        return inner
        
@add_emoticon('-_-')    
def say_hi(name) :
    print(f'hi {name}!')
    
say_hi('Jason')
print(say_hi.count)                          # 함수 호출 회수 출력

add_emoticon 클래스에 함수 호출 회수를 저장 할 수 있는 필드를 만들어 최종적으로 함수를 몇번 호출 했는지 출력하도록 했다. 여기서 중요한 부분은 함수호출 회수가 아니라, add_emoticon이라는 데코레이터를 클래스 객체를 통해 callable object로 만들므로써 데코레이터가 상태를 저장하고 있다는 것이다.

마치며

이상으로 클래스 데코레이터를 만드는 방법을 살펴 보았다. 다음 포스트는 클래스의 상속에 관하여 정리 해볼까 한다. 이제 클래스에 대한 강좌가 거의 마지막에 가까워 지고 있다. 조금만 더 힘내자.

부록 1. 같이 보면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!