ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] Tip - 메타클래스로 클래스의 존재를 등록
    언어/파이썬 & 장고 2016. 12. 6. 20:33

    메타클래스를 사용하는 또 다른 일반적인 사례는 프로그램에 있는 타입을 자동으로 등록하는 것입니다. 등록은 간단한 식별자를 대응하는 클래스에 매핑하는 역방향 조회를 수행할 때 유용합니다.

    예를 들어 파이썬 객체를 직렬화한 표현을 JSON으로 구현한다고 가정합니다. 객체를 얻어와 JSON 문자열로 변환할 방법이 필요합니다. 다음은 새엉자 파라미터를 저장하고 JSON 딕셔너리로 변환하는 기반 클래스를 범용적으로 정의한 것입니다.

    import json


    class Serializable:
    def __init__(self, *args):
    self.args = args

    def serialize(self):
    return json.dumps({'args': self.args})


    class Point2D(Serializable):
    def __init__(self, x, y):
    super().__init__(x, y)
    self.x = x
    self.y = y

    def __repr__(self):
    return 'Point2D(%d, %d)' % (self.x, self.y)


    point = Point2D(5, 3)
    print('Object: ', point)
    print('Serialized: ', point.serialize())

    # 결과
    # Object: Point2D(5, 3)
    # Serialized: {"args": [5, 3]}


    위와같이 Point2D처럼 간단한 불변 자료 구조를 문자열로 쉽게 직렬화할 수 있습니다. 위 JSON 문자열을 역직렬화해서 JSON이 표현하는 Point2D 객체를 생성해야 합니다. 이번에는 Serializable 부모 클래스에 있는 데이터를 역직렬화하는 또 다른 클래스를 정의합니다.

    import json
    
    
    class Serializable:
        def __init__(self, *args):
            self.args = args
    
        def serialize(self):
            return json.dumps({'args': self.args})
    
    
    class Deserializable(Serializable):
        @classmethod
        def deserialize(cls, json_data):
            params = json.loads(json_data)
            return cls(*params['args'])
    
    
    class BetterPoint2D(Deserializable):
        def __init__(self, x, y):
            super().__init__(x, y)
            self.x = x
            self.y = y
    
        def __repr__(self):
            return 'Point2D(%d, %d)' % (self.x, self.y)
    
    
    point = BetterPoint2D(5, 3)
    print('Before:  ', point)
    data = point.serialize()
    print('Serialized: ', data)
    after = BetterPoint2D.deserialize(data)
    print('After:   ', after)
    
    # 결과
    # Before:   Point2D(5, 3)
    # Serialized:  {"args": [5, 3]}
    # After:    Point2D(5, 3)


    위 방법의 문제는 직렬화된 데이터에 대응하는 타입(예를 들어 Point2D, BetterPoint2D)를 미리 알고 있을 때만 동작한다는 점입니다. 이상적으론 JSON으로 직렬화되는 클래스를 많이 갖추고 그중 어떤 클래스든 대응하는 파이썬 객체로 역직렬화하는 공통 함수를 하나만 두려고 할 것입니다.  이렇게 만들려면 직렬화할 객체의 클래스 이름을 JSON 데이터에 포함하면 됩니다.

    import json
    
    
    class BetterSerializable:
        def __init__(self, *args):
            self.args = args
    
        def serialize(self):
            return json.dumps({
                'class': self.__class__.__name__,
                'args': self.args,
            })
    
    
    registry = {}
    
    
    def register_class(target_class):
        registry[target_class.__name__] = target_class
    
    
    def deserialize(data):
    	# deserialize가 항상 제대로 동작함을 보장하려면 추후에 역직렬화할 법한 모든 클래스에 register_class를 호출해야 함
        params = json.loads(data)
        name = params['class']
        target_class = registry[name]
        print(params['args'])
        return target_class(*params['args'])
    
    
    class EvenBetterPoint2D(BetterSerializable):
        def __init__(self, x, y):
            super().__init__(x, y)
            self.x = x
            self.y = y
    
    
    register_class(EvenBetterPoint2D)
    
    point = EvenBetterPoint2D(5, 3)
    print('Before:  ', point)
    data = point.serialize()
    print('Serialized: ', data)
    after = deserialize(data)
    print('After:   ', after)
    
    # 결과
    # Before:   <__main__.EvenBetterPoint2D object at 0x102abed68>
    # Serialized:  {"args": [5, 3], "class": "EvenBetterPoint2D"}
    # After:    <__main__.EvenBetterPoint2D object at 0x102ae8f98>


    위처럼 코드를 개발하면 어떤 클래스를 담고 있는지 몰라도 임의의 JSON 문자열을 역직렬화할 수 있습니다. 하지만 문제는 register_class를 호출하는 일을 까먹을 수 있다는 점입니다. 이는 등록을 잊은 클래스의 객체를 런타임에 역직렬화하려 할 때 코드가 중단되는 원인이 됩니다.

    BetterSerializable을 상속해서 서브클래스를 만들더라도 class 문의 본문 이후에 register_class를 호출하지 않으면 실제로 모든 기능을 사용하진 못합니다. 이 방법은 오류가 일어날 가능성이 높습니다. 파이썬 3 클래스 데코레이터를 사용할 때도 이런 누락이 있을 수 있습니다.

    프로그래머가 의도한 대로 BetterSerializable을 사용하고 모든 경우에 register_class가 호출된다고 확신하는 방법은 메타클래스를 사용해 서브클래스가 정의될 때 class문을 가로채는 방법으로 만들 수 있습니다. 메타클래스로 클래스 본문이 끝나자마자 새 타입을 등록하면 됩니다.

    import json
    
    registry = {}
    
    
    def register_class(target_class):
        registry[target_class.__name__] = target_class
    
    
    class Meta(type):
        def __new__(meta, name, bases, class_dict):
            cls = type.__new__(meta, name, bases, class_dict)
            register_class(cls)
            return cls
    
    
    class BetterSerializable:
        def __init__(self, *args):
            self.args = args
    
        def serialize(self):
            return json.dumps({
                'class': self.__class__.__name__,
                'args': self.args,
            })
    
    
    class RegisteredSerializable(BetterSerializable, metaclass=Meta):
        pass
    
    
    def deserialize(data):
        params = json.loads(data)
        name = params['class']
        target_class = registry[name]
        print(params['args'])
        return target_class(*params['args'])
    
    
    class Vector3D(RegisteredSerializable):
        def __init__(self, x, y, z):
            super().__init__(x, y, z)
            self.x, self.y, self.z = x, y, z
    
    
    v3 = Vector3D(10, -7, 3)
    print('Before: ', v3)
    data = v3.serialize()
    print('Serialized: ', data)
    print('After:   ', deserialize(data))
    
    
    # 결과
    # Before:  <__main__.Vector3D object at 0x1006bef28>
    # Serialized:  {"class": "Vector3D", "args": [10, -7, 3]}
    # [10, -7, 3]
    # After:    <__main__.Vector3D object at 0x1006f1eb8>

    RegisteredSerializable의 서브클래스를 정의할 때 register_class가 호출되어 deserialize가 항상 기대한 대로 동작할 것이라고 확신할 수 있습니다.

    메타클래스를 사용해 클래스를 등록하면 상속 트리가 올바르게 구축되어 있는 한 클래스 등록을 놓치지 않습니다. 앞에서 본 것처럼 직렬화에 잘 동작하며 데이터베이스 객체 관계 매핑(ORM), 플러그인 시스템, 시스템 후크에도 적용할 수 있습니다.

    요약

    클래스 등록은 모듈 방식의 파이썬 프로그램을 만들 때 유용한 패턴

    메타클래스를 이용하면 프로그램에서 기반 클래스로 서브클래스를 만들 때마다 자동으로 등록 코드를 실행할 수 있음

    메타클래스를 이용해 클래스를 등록하면 등록 호출을 절때 빠뜨리지 않으므로 오류를 방지


    댓글