메아리 저널

genobject​

파이썬의 __new____init__의 구분은 개인적으로 파이썬에서 짜증나는 몇 안 되는 구석 중 하나이다. 정의에 따라, 기본적으로 클래스 C를 호출했을 때 실제로 호출되는 함수 C.__call__의 구현은 다음과 같이 된다.

def __call__(cls, *args, **kwargs):
    obj =  cls.__new__(*args, **kwargs)
    if isinstance(obj, cls):
        obj.__init__(*args, **kwargs)
    return obj

요컨대 __new__는 객체를 생성하지만 값을 집어 넣지는 않고, 만약 그 반환값이 원래 클래스의 인스턴스이면 __init__를 한 번 더 호출해서 실제 값을 채워 넣는 것이다. 그러나 이 모델은 클래스 C에 자식 클래스 D가 존재하며 둘 다 __new__를 갈아 치웠을 경우 완전히 깨져 버린다.

이를테면, 다른 전혀 상관 없는 클래스 X가 존재하고 C(X(a), b)D(a, b)를 똑같은 의미로 만들고 싶은데 C(X(X(a)), b)는 그대로 남겨 뒀으면 좋겠다고 하자. (요컨대 클래스 D는 클래스 C의 특수화인데 편의를 위해 추가적인 메소드를 더 제공하는 것이다. 자동으로 canonicalization을 한다고 생각할 수도 있겠다.) 아무 생각 없이 구현하면 C.__new__에서는 첫 인자가 정확히 X(a) 꼴이면 D의 인스턴스를 생성해서 제공하고, D.__new__에서는 첫 인자가 정확히 a 꼴이 아니면 C의 인스턴스를 생성해서 제공하게 된다. 그러나! C(X(a), b)를 실제로 호출하면 C.__new__는 D의 인스턴스를 반환하지만 이는 C의 인스턴스이기도 하므로 D.__init__가 한 번 더, 그것도 원치 않는 인자(X(a), b)로 불리게 되어 버린다. 이걸 막기 위해 D.__init__에서 첫 인자를 특수 처리하면 이제 D의 생성자만으로 생성 불가능한 경우가 생겨 버린다.

이걸 해결하는 “표준적인” 방법은 메타클래스를 써서 C.__metaclass__.__call__D.__metaclass__.__call__을 완전히 오버라이드하는 것이다. 하지만 파이썬의 메타클래스는 사용이 썩 좋은 편이 아니며, 특히 이런 류의 코드가 많을 수록 클래스 하나당 새 메타클래스를 따로 만들어야 하므로 구찮아 뒈질 것 같다. 그래서 종국에는 이따위 걸 만들어 버렸다.

class gentype(type):
    def __new__(self, name, bases, dict):
        if '__slots__' not in dict:
            dict['__slots__'] = ()
        if '__gen__' in dict:
            dict['__gen__'] = staticmethod(dict['__gen__'])
        return type.__new__(self, name, bases, dict)

    def __call__(self, *args, **kwargs):
        obj = self.__gen__(self, *args, **kwargs)
        if obj is NotImplemented:
            obj = type.__call__(self, *args, **kwargs)
        return obj

class genobject(object):
    __metaclass__ = gentype
    __gen__ = type.__call__

이 genobject로부터 상속받는 모든 클래스는 생성자가 호출되면 모든 것에 앞서 __gen__이라는 특수 메소드를 먼저 부른다. 만약 이 메소드가 NotImplemented를 반환하면 그때서야 원래대로 __new____init__를 사용하여 새 객체를 만든다. (다만 genobject의 기본 __gen__ 구현은 성능-_-을 생각하여 그냥 type.__call__로 되어 있다.) __new__가 파이썬에서 내부적으로 staticmethod로 처리되듯, __gen__도 마찬가지로 별도로 처리하지 않아도 staticmethod가 된다.

이 코드는 주어진 인자에 따라 인스턴스 생성 과정을 완전히 갈아 엎어야 할 때 편리하게 사용될 수 있을 것이다. (다만 약간 느려지는 것 같긴 하더라.) 너무 간단한 코드이니만큼 public domain으로 공개하니 알아서 잘 뜯어 고쳐 쓰시길 바란다.


노트들

  1. khris-kr reblogged this from arachneng and added:
    파이썬에 대한 이해도가 심각하게 부족함을 느꼈다. (사실 이전부터 틈만 나면 느꼈지만…)
  2. arachneng posted this
텀블러를 씁니다.