파이썬 데코레이터

Posted 2007. 7. 28. 03:56
오래간 만에 포스팅입니다. 요즘 3-4시간 밖에 못자는 야근을 거듭하느라 블로깅 할 여유가 전혀 없었습니다. 근데 또 한동안 글을 안 쓰니깐 손이 근질근질해서 이제는 짧은 내용이라도 하루에 1-2개 정도는 반드시 남겨야 겠다는 다짐을 해봅니다. 민감한 문제는 안 건드리고 주로 가벼운 프로그래밍 얘기만 하는 걸로요.

요즘 파이썬으로 코딩하는 일이 많아지면서, 파이썬 언어를 조금 더 진지하게 보고 있습니다. 관련해서 마소 8월호에 파이썬 데코레이터(decorator)를 이용한 파이썬 언어 확장을 주제로 글을 쓰기도 했습니다. 조금 있으면 나올테니깐 관심 있으신 분들은 서점에서 잠깐 보시고요. 까멜레오 프로젝트에도 파이썬 데코레이터를 자바의 어노테이션(annotation)처럼 활용하고 있습니다.


예를 들어서 어떤 함수를 수정해야 겠다고 표시해 놓을 때 다음과 같은 @fixme 데코레이터를 사용합니다.

   @fixme("""Take 'border-width' property into account.""")
    def __init__(self):
        ...


코드를 작성하다가 머리가 아파서 아직 구현을 안 한 경우 나중에 구현을 다 한 줄 알고 착각하는 걸 방지하기 위해 다음과 같이 @notimplemented 데코레이터를 붙여놓습니다.

    @notimplemented
    def reorder_child(self, child, position):
        ...


코드를 짜기는 짰는데, 이거 구현이 엉망이라 잠시 해당 메서드를 부르더라도 아무 일도 하지 않게 만드려면 @nop만 붙여주면 됩니다.


    @nop
    def do_many_things(self):
        ...


조금 복잡한 어노테이션으로는 C#의 new와 override 변경자(modifier)를 파이썬으로 데코레이터로 구현해 쓰고 있습니다. @new는 상위 클래스의 메소드를 오버라이드하지 않는 새로운 메소드를 정의함을 의미하고, @override는 반대로 반드시 상위 메소드를 오버라이드한다는 의도를 드러냅니다.


데코레이터가 좋은 점은 코드를 봤을 때 가독성을 높여주는 효과도 있지만, 실제로 코드 수행 시에 여러 가지 일을 해줄 수 있다는 점에 있습니다. 예를 들어 @fixme(comment)는 함수가 호출될 때 콘솔에 comment 내용을 WARNING으로 출력해줘서 항상 이 함수를 고쳐야하겠구나라는 경각심을 가질 수 있게 해줍니다. 반대로 @notimplemented를 붙여놓고는 다 구현한 줄 알고 안심한 개발자에게는 해당 메서드가 불리자 말자 구현 안했다고 에러 메시지를 낸 후 배째고 수행을 중지합니다. @nop은 함수 바디를 무시하고 아무 것도 안 하고 리턴하도록 바꿔버리고요.

@new와 @override는 파이썬의 동적인 특징을 최대한 활용해 메서드가 불리는 순간에 self의 클래스를 구한 후에 클래스의 슈퍼클래스들을 타고 올라가서 같은 이름의 메서드가 있는지를 찾아냅니다. 이렇게 찾아낸 정보로 만약 @new인데 상위 클래스에 같은 이름의 메서드가 있거나 @override인데 상위 클래스에 해당 메서드가 없으면 에러를 출력해 주는 것이죠.

별 것 아닌 확장이지만, 컴파일 언어와 달리 조금만 방심하면 버그가 있어도 모른 채 넘어가는 파이썬 코딩과 디버깅에는 많은 도움이 됩니다. 다음은 @new와 @override의 소스 코드입니다. 버그가 있을지도 모르니 유심히 살펴보시고 더 좋은 아이디어 있으면 환영합니다.


class NoOverrideMethodFoundError(Exception):
    """Exception class for override decorator."""

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

    def __str__(self):
        return repr(self.message)

def override(func):
    """If there is no suitable method found to override,
    throw an exception."""

    def new_func(self, *args):
        try:
            getattr(new_func, "__override_checked__")
            new_func.__override_checked__ = True
        except:
            method = None
            for base in self.__class__.__bases__:
                try:
                    method = getattr(base, func.__name__)
                    break
                except AttributeError:
                    pass

            if method == None:
                raise NoOverrideMethodFoundError, \
                        "%s is not found to in %s' base classes." \
                        % (func.__name__, self.__class__.__name__)

        return func(self, *args)

    return new_func

#------------------------------------------------------------------------------
# @new
#------------------------------------------------------------------------------

class AccidentalOverrideError(Exception):
    """Exception class for new decorator."""

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

    def __str__(self):
        return repr(self.message)

def new(func):
    """Prevent accidental method overriding.

    When you use @new decorator, your intention is to declare a
    new method which does not accidentally override the base method.
    """

    def new_func(self, *args):
        try:
            getattr(new_func, "__new_checked__")
            new_func.__new_checked__ = True
        except:
            for base in self.__class__.__bases__:
                try:
                    method = getattr(base, func.__name__)
                    raise AccidentalOverrideError, \
                    "%s.%s accidentally overrided the method %s.%s." \
                        % (self.__class__.__name__, func.__name__, \
                        base.__name__, func.__name__)
                except AttributeError:
                    pass

        return func(self, *args)

    return new_func