ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] Tip - 믹스인 유틸리티 클래스에만 다중 상속을 사용
    언어/파이썬 & 장고 2016. 10. 26. 21:00

    파이썬은 다중 상속을 다루기 쉽게 하는 기능을 내장한 객체지향 언어입니다. 다중 상속으로 얻는 편리함과 캡슐화가 필요하다면 대신 믹스인(mix-in)을 작성하는 방안을 고려해야 합니다. 믹스인이란 클래스에서 제공해야 하는 추가적인 메서드만 정의하는 작은 클래스를 말합니다. 믹스인 클래스는 자체의 인스턴스 속성(attribute)를 정의하지 않으며 __init__ 생성자를 호출하도록 요구하지 않습니다.

    Mix-In 상세 설명


    파이썬에서의 믹스인은믹스 인은 특별한 종류의 다중 상속입니다. 믹스인이 사용되는 두 가지 주요 상황이 있습니다.

    한 클래스에 대해 많은 선택 기능을 제공할 때

    # 많은 다른 클래스에서 하나의 특정 기능을 사용하려고합니다.
    
    class HasMethod1(object):
        def method(self):
            return 1
    
    class HasMethod2(object):
        def method(self):
            return 2
    
    class UsesMethod10(object):
        def usesMethod(self):
            return self.method() + 10
    
    
    class UsesMethod20(object):
        def usesMethod(self):
            return self.method() + 20
    
    class C1_10(HasMethod1, UsesMethod10): 
        pass
    class C1_20(HasMethod1, UsesMethod20): 
        pass
    class C2_10(HasMethod2, UsesMethod10): 
        pass
    class C2_20(HasMethod2, UsesMethod20): 
        pass
    
    print(C1_10().usesMethod() == 11) # UsesMethod10클래스의 usesMethod()가 호출되고 HasMethod1클래스의 method()가 호출되어 1 + 10이 됨
    print(C1_20().usesMethod() == 21) # UsesMethod20클래스의 usesMethod()가 호출되고 HasMethod1클래스의 method()가 호출되어 1 + 20이 됨
    print(C2_10().usesMethod() == 12) # UsesMethod10클래스의 usesMethod()가 호출되고 HasMethod1클래스의 method()가 호출되어 2 + 10이 됨
    print(C2_20().usesMethod() == 22) # UsesMethod20클래스의 usesMethod()가 호출되고 HasMethod2클래스의 method()가 호출되어 2 + 20이 됨
    
    # 결과
    # True
    # True
    # True
    # True

    많은 다른 클래스에서 하나의 특정 기능을 사용하려고 할 때

    # 한 클래스에 대해 많은 선택 기능을 제공하려고합니다.
    class HasMethod1(object):
        def hasmethod1(self):
            return 1
    
    
    class HasMethod2(object):
        def hasmethod2(self):
            return 2
    
    
    class UsesMethod10(object):
        def usesmethod10(self):
            return self.method() + 10
    
    
    class UsesMethod20(object):
        def usesmethod20(self):
            return self.method() + 20
    
    
    class C1(HasMethod1, HasMethod2, UsesMethod10, UsesMethod20):
        def method(self):
            return 10
    
    
    print(C1().hasmethod1(), C1().hasmethod2(), C1().usesmethod10())
    
    # 결과
    # 1 2 20


    ruby의 경우

    module M1
      def m1_m
        p "m1_m"
      end
    end
    module M2
      def m2_m
        p "m2_m"
      end
    end
    class C
      include M1, M2                                                                                        
    end
    c = C.new()
    c.m1_m()
    c.m2_m()

    class C 의 include 부분이 믹스인.

    먼저 모듈 M1과 M2가 선언되며, 클래스 C가 이 모듈들을 include한 다음, C 클래스의 인스턴스를 만들어 주고 include한 두 모듈들의 메소드를 호출하면 에러없이 정상적으로 동작하게 됨

    따라서, C가 M1과 M2의 메소드를 포함하고 있는 것처럼 행동하는 것이 믹스인의 핵심

    파이썬에서는 타입과 상관없이 객체의 현재 상태를 간단하게 조사할 수 있어서 믹스인을 쉽게 작성할 수 있습니다. 동적 조사를 이용하면 많은 클래스에 적용할 수 있는 범용 기능을 믹스인에 한 번만 작성하면 됩니다. 믹스인들을 조합하고 계층으로 구성하면 반복 코드를 최소화하고 재사용성을 극대화할 수 있습니다.


    예를 들어, 파이썬 객체를 메모리 내부 표현에서 직렬화(serialization)용 딕셔너리로 변환하는 기능이 필요하다고 가정합니다. 이 기능을 모든 클래스에서 사용할 수 있게 범용으로 작성하도록 만들어 보겠습니다.

    다음은 상속받는 모든 클래스에 추가될 새 공개 메서드로 이 기능을 구현하는 믹스인입니다.

    class ToDictMixin:
        def to_dict(self):
            # __dict__은 상속받은 값들에 대해 dict타입으로 보여줌
            print('Todict', self.__dict__)
            return self._traverse_dict(self.__dict__)
    
        def _traverse_dict(self, instance_dict):
            output = {}
            for key, value in instance_dict.items():
                output[key] = self._traverse(key, value)
            return output
    
        def _traverse(self, key, value):
            """
            세부 구현은 직관적이며 hasattr을 사용한 동적 속성 접근, isinstance를 사용한 동적 타입 검사, 인스턴스 딕셔너리 __dict__를 이용한 클래스
            """
    
            if isinstance(value, ToDictMixin):
                return value.to_dict()
            elif isinstance(value, dict):
                return self._traverse_dict(value)
            elif isinstance(value, list):
                return [self._traverse(key, i) for i in value]
            elif hasattr(value, '__dict__'):
                return self._traverse_dict(value.__dict__)
            else:
                return value
    
    
    class BinaryTree(ToDictMixin):
        def __init__(self, value, left=None, right=None):
            self.value = value
            self.left = left
            self.right = right
    
    
    tree = BinaryTree(10, left=BinaryTree(7, right=BinaryTree(9)))
    
    print(tree.to_dict())
    
    # 결과
    # {'value': 10, 'right': {'value': 13, 'right': None, 'left': {'value': 11, 'right': None, 'left': None}}, 'left': {'value': 7, 'right': {'value': 9, 'right': None, 'left': None}, 'left': None}}

    이와 같이 수많은 파이썬 객체를 딕셔너리로 손쉽게 변환할 수 있습니다.


    믹스인의 가장 큰 장점은 범용 기능을 교체할 수 있게 만들어서 필요할 때 동작을 오버라이드할 수 있다는 점입니다. 예를 들어 다음은 부모 노드에 대한 참조를 저장하는 BinaryTree의 서브클래스입니다. 이 순환 참조는 ToDictMixin.to_dict의 기본 구현이 무한 루프에 빠지게 만듭니다. ToDictMixin 클래스 내의 _traverse()함수에서 if isinstance(value, ToDictMixin): 이 부분에서 parent는 ToDictMixin 클래스의 자식클래스이기 때문에 to_dict()함수를 재 호출하고 다시 _traverse()함수의 조건에 걸리게 됩니다.

    class BinaryTreeWithParent(BinaryTree):
        def __init__(self, value, left=None, right=None, parent=None):
            super().__init__(value, left=left, right=right)
            self.parent = parent


    해결책은 BinaryTreeWithParent 클래스에서 ToDictMixin._traverse 메서드를 오버라이드해서 믹스인이 순환에 빠지지 않도록 필요한 값만 처리하게 하는 것입니다. 다음은 _traverse메서드를 오바라이드해서 부모를 탐색하지 않고 부모의 숫자 값만 꺼내오게 만든 예제입니다.

    class BinaryTreeWithParent(BinaryTree):
        def __init__(self, value, left=None, right=None, parent=None):
            super().__init__(value, left=left, right=right)
            self.parent = parent
    
        def _traverse(self, key, value):
            if (isinstance(value, BinaryTreeWithParent) and key == 'parent'):
                return value.value  # 순환방지
            else:
                return super()._traverse(key. value)


    순환 참조 속성을 따라가지 않으므로 BinaryTreeWithParent.to_dict를 호출하는 코드는 문제 없이 동작합니다.

    class ToDictMixin:
        def to_dict(self):
            # __dict__은 상속받은 값들에 대해 dict타입으로 보여줌
            return self._traverse_dict(self.__dict__)
    
        def _traverse_dict(self, instance_dict):
            output = {}
            for key, value in instance_dict.items():
                output[key] = self._traverse(key, value)
            return output
    
        def _traverse(self, key, value):
            """
            세부 구현은 직관적이며 hasattr을 사용한 동적 속성 접근, isinstance를 사용한 동적 타입 검사, 인스턴스 딕셔너리 __dict__를 이용한 클래스
            """
    
            if isinstance(value, ToDictMixin):
                return value.to_dict()
            elif isinstance(value, dict):
                return self._traverse_dict(value)
            elif isinstance(value, list):
                return [self._traverse(key, i) for i in value]
            elif hasattr(value, '__dict__'):
                return self._traverse_dict(value.__dict__)
            else:
                return value
    
    
    class BinaryTree(ToDictMixin):
        def __init__(self, value, left=None, right=None):
            self.value = value
            self.left = left
            self.right = right
    
    
    class BinaryTreeWithParent(BinaryTree):
        def __init__(self, value, left=None, right=None, parent=None):
            super().__init__(value, left=left, right=right)
            self.parent = parent
    
        def _traverse(self, key, value):
            if (isinstance(value, BinaryTreeWithParent) and key == 'parent'):
                return value.value  # 순환방지
            else:
                return super()._traverse(key, value)
    
    
    root = BinaryTreeWithParent(10)
    root.left = BinaryTreeWithParent(7, parent=root)
    root.left.right = BinaryTreeWithParent(9, parent=root.left)
    print(root.to_dict())
    
    # 결과
    # {'parent': None, 'left': {'parent': 10, 'left': None, 'right': {'parent': 7, 'left': None, 'right': None, 'value': 9}, 'value': 7}, 'right': None, 'value': 10}


    BinaryTreeWithParent._traverse를 정의한 덕분에 BinaryTreeWithParent 타입의 속성이 있는 클래스라면 무엇이든 자동으로 ToDictMixin을 동작할 수 있게 되었습니다. 

    class ToDictMixin:
        def to_dict(self):
            # __dict__은 상속받은 값들에 대해 dict타입으로 보여줌
            return self._traverse_dict(self.__dict__)
    
        def _traverse_dict(self, instance_dict):
            output = {}
            for key, value in instance_dict.items():
                output[key] = self._traverse(key, value)
            return output
    
        def _traverse(self, key, value):
            """
            세부 구현은 직관적이며 hasattr을 사용한 동적 속성 접근, isinstance를 사용한 동적 타입 검사, 인스턴스 딕셔너리 __dict__를 이용한 클래스
            """
    
            if isinstance(value, ToDictMixin):
                return value.to_dict()
            elif isinstance(value, dict):
                return self._traverse_dict(value)
            elif isinstance(value, list):
                return [self._traverse(key, i) for i in value]
            elif hasattr(value, '__dict__'):
                return self._traverse_dict(value.__dict__)
            else:
                return value
    
    
    class BinaryTree(ToDictMixin):
        def __init__(self, value, left=None, right=None):
            self.value = value
            self.left = left
            self.right = right
    
    
    class BinaryTreeWithParent(BinaryTree):
        def __init__(self, value, left=None, right=None, parent=None):
            super().__init__(value, left=left, right=right)
            self.parent = parent
    
        def _traverse(self, key, value):
            if (isinstance(value, BinaryTreeWithParent) and key == 'parent'):
                return value.value  # 순환방지
            else:
                return super()._traverse(key, value)
    
    
    class NamedSubTree(ToDictMixin):
        def __init__(self, name, tree_with_parent):
            self.name = name
            self.tree_with_parent = tree_with_parent
    
    
    root = BinaryTreeWithParent(10)
    root.left = BinaryTreeWithParent(7, parent=root)
    root.left.right = BinaryTreeWithParent(9, parent=root.left)
    # print(root.to_dict())
    
    my_tree = NamedSubTree('foobar', root.left.right)
    print(my_tree.to_dict()) # 무한 루프를 돌지 않음
    
    
    # 결과
    # {'name': 'foobar', 'tree_with_parent': {'parent': 7, 'value': 9, 'left': None, 'right': None}}


    믹스인을 조합할 수도 있습니다. 예를 들어 어떤 클래스에도 동작하는 범용 JSON 직렬화를 제공하는 믹스인이 필요하다고 가정합니다. 이 믹스인은 클래스에 to_dict 메서드(ToDictMixin 클래스에서 제공할 수도 있고 그렇지 않을 수도 있음)가 있다고 가정하고 만들면 됩니다.

    import json
    
    
    class JsonMixin:
        @classmethod
        def from_json(cls, data):
            kwargs = json.loads(data)
            return cls(**kwargs) # 인스턴스 속성으로 값을 넘겨줌
    
        def to_json(self):
            return json.dumps(self.to_dict())

    JsonMixin 클래스가 어떻게 인스턴스 메서드와 클래스 메서드를 둘 다 정의하는지 주목해야 합니다. 믹스인을 이용하면 이 두 종류의 동작을 추가할 수 있습니다. 예제에서 JsonMixin의 요구 사항은 클래스에 to_dict 메서드가 있고 해당 클래스의 __init__ 메서드에서 키워드 인수를 받는다는 것 뿐입니다.


    이 믹스인을 이용하면 짧은 반복 코드로 JSON으로 직렬화하고 JSON에서 역직렬화하는 유틸리티 클래스의 계층 구조를 간단하게 생성할 수 있습니다. 예를 들어 다음은 데이터센터 토폴로지를 구성하는 부분들을 표현하는 데이터 클래스의 계층입니다.

    class DatacenterRack(ToDictMixin, JsonMixin):
        def __init__(self, switch=None, machines=None):
            self.switch = Switch(**switch)
            self.machines = [Machine(**kwargs) for kwargs in machines]
    
    
    class Switch(ToDictMixin, JsonMixin):
        pass
    
    
    class Machine(ToDictMixin, JsonMixin):
        pass


    이 클래스들을 JSON으로 직렬화하고 JSON에서 역직렬화하는 방법은 간단합니다. 여기서는 데이터가 직렬화와 역직렬화를 통해 원래 상태가 되는지 검증합니다.

    serialized = """{"switch": {"ports": 5, "speed": 1e9},
                  "machines": [{"cores": 8, "ram": 32e9, "disk": 5e12},
                               {"cores": 4, "ram": 16e9, "disk": 1e12},
                               {"cores": 2, "ram": 4e9, "disk": 500e9}]}"""
    
    deserialized = DatacenterRack.from_json(serialized) # deserialized의 리턴은 인스턴스속성이므로 self값에 DatacenterRack()의 딕셔너리 값이 포함되어 있음
    
    # 호출 시 JsonMixin이 호출되고 cls(**kwargs)로 json이 DatacenterRack()의 생성자인 __init__을 호출하게 됨. 다음 json의 key에 맞게 switch 키인 값이 switch 변수에, machines 키의 값이 machines 변수에 담김
    
    
    roundtrip = deserialized.to_json()
    assert json.loads(serialized) == json.load(roundtrip)
    
    
    
    

    이런 믹스인을 사용할 때는 클래스가 객체 상속 계층의 상위에서 이미 JsonMixin을 상속받고 있어도 괜찮습니다. 결과로 만들어지는 클래스는 같은 방식으로 동작할 것입니다.

    요약

    믹스인 클래스로 같은 결과를 얻을 수 있다면 다중 상속 사용 금지

    인스턴스 수준에서 동작을 교체할 수 있게 만들어서 믹스인 클래스가 요구할 때 클래스 별로 원하는 동작을 하게 만드는 것이 좋음

    간단한 동작들로 복잡한 기능을 생성하려면 믹스인을 조합


    댓글