Effective Python Item 41. 기능을 합성할 때는 믹스인 클래스를 사용하라

    들어가기 전

     


    요약

    • 믹스인 클래스는 다음을 의미함.
      • 공통으로 사용할 메서드만 몇개 구현함. 
      • 믹스인 클래스는 멤버 변수가 없음. 따라서 __init__()를 구현할 필요가 없음. 
    • 파이썬의 다중상속은 디버깅에 어려움을 가져옴. 따라서 꼭 필요한 경우가 아니라면, 다중상속 대신 믹스인 클래스를 합성하자. 
    • 믹스인 클래스가 제공하는 메서드는 필요한 경우 오버라이드 해서 사용할 수 있음. 
    • 믹스인은 클래스 수준(@classmethod)의 기능 / 인스턴스 수준의 기능을 제공할 수 있음. 

     


    Item41. 기능을 합성할 때는 믹스인 클래스를 사용하라

    믹스인 클래스는 멤버 변수가 없으며, 자식 클래스에서 사용할 몇 개의 메서드만 제공하는 클래스다. 믹스인 클래스를 사용하면 여러 클래스에 공통되는 기능을 손쉽게 제공할 수 있다는 장점이 있다. 

    # 믹스인 클래스 예시 
    class ToDictMixin:
        def to_dict(self):
            return self._traverse_dict(self.__dict__)

    또한 기능을 합성할 때, 다중상속 대신 믹스인을 사용하는 것이 훨씬 낫다. 다중상속을 하게 되면 MRO에 의해서 오버라이딩, 호출 순서 등이 결정되는데 이것을 고려하기가 어렵다. 그렇기 때문에 단순히 기능의 합성만 필요하다면, 다중상속 대신 믹스인 클래스를 이용하는 것이 더 간결하고 유지보수하기 쉬운 코드가 된다. 

    또한 믹스인 클래스는 클래스 레벨(@classmethod) / 인스턴스 레벨로도 모두 제네릭한 기능을 제공할 수 있다. 그리고 믹스인 클래스로부터 메서드를 제공 받았을 때, 필요한 메서드만 오버라이드 해서 원하는 기능을 제공할 수도 있다. 

     


    코드1. 믹스인 클래스 예시 

    class ToDictMixin:
        def to_dict(self):
            return self._traverse_dict(self.__dict__)
    
        def _traverse_dict(self, instance_dict):
            return {key: self._traverse(key, value) for key, value in instance_dict.items()}
    
        def _traverse(self, key, value):
            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
    1. 클래스는 믹스인 클래스의 예시다. 
    2. 이 클래스는 인스턴스가 가지고 있는 멤버변수들을 딕셔너리로 바꿔서 반환해주는 기능을 제공한다. 
    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())
    1. 이 믹스인 클래스는 클래스 레벨 / 인스턴스 레벨의 제네릭 메서드를 제공한다. 
    2. 이 클래스는 json으로부터 인스턴스를 만듦 / 딕셔너리를 json으로 만드는 제네릭 메서드를 제공한다. 

    위에서 볼 수 있듯이, 믹스인 클래스의 특징은 멤버 변수가 존재하지 않고, 적은 수의 제네릭 메서드가 캡슐화 되어 제공된다는 것이다. 

     


    코드2. 믹스인 클래스 사용 예시 (이진트리) 

    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)),
        right=BinaryTree(13, left=BinaryTree(11)))
    print(tree.to_dict())
    1. 이진트리 클래스를 선언한다. 
    2. 이진트리 클래스는 ToDictMixin 클래스를 상속받음. 

    이진트리 클래스가 ToDictMixin 클래스를 상속받기 때문에 멤버 변수들을 딕셔너리로 변경해주는 기능이 제공된다. tree.to_dict()를 호출한 결과는 아래와 같다.

    {'value': 10, 'left': {'value': 7, 'left': None, 'right': {'value': 9, 'left': None, 'right': None}}, 'right': {'value': 13, 'left': {'value': 11, 'left': None, 'right': None}, 'right': None}}

    믹스인 클래스를 구현해서 제공하면, 제네릭하게 사용될 수 있는 기능을 제공할 수 있다. (심지어 부담스럽지 않게!) 

     


    코드3. 믹스인 클래스 사용 예시 (오버라이드) 

    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())
    1. BinaryTreeWithParent는 BinaryTree를 상속받은 클래스다.
    2. 부모 클래스의 to_dict를 그대로 사용하는 경우 순환참조로 인해 무한 재귀가 발생한다. 
    3. 2번 문제를 해결하기 위해 부모 클래스의 _traverse() 메서드를 오버라이드해서 문제를 해결했다. 

    이처럼 부모 클래스 (믹스인 클래스)가 제공하는 기능을 그대로 사용하는 대신, 메서드 오버라이드를 활용해 다형성을 구현할 수도 있게 된다. 

     


    코드4. 믹스인 클래스의 합성

    class ToDictMixin:
        def to_dict(self):
            return self._traverse_dict(self.__dict__)
    
        def _traverse_dict(self, instance_dict):
            return {key: self._traverse(key, value) for key, value in instance_dict.items()}
    
        def _traverse(self, key, value):
            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 JsonMixin:
    
        @classmethod
        def from_json(cls, data):
            kwargs = json.loads(data)
            return cls(**kwargs)
    
        def to_json(self):
            return json.dumps(self.to_dict())

    ToDictMixin / JsonMixin 두 가지 믹스인 클래스가 있다. 클래스들은 두 개 이상의 믹스인 클래스를 상속받아, 믹스인 클래스가 제공하는 기능을 사용할 수 있다. 

    1. JsonMixin 클래스는 to_dict()라는 메서드가 필요하다.
    2. to_dict() 메서드는 ToDictMixin 클래스가 제공하는 것이다. 따라서 JsonMixin 클래스는 ToDictMixin 클래스와 함께 사용되어야 할 것이다. 
    class DatacenterRack(ToDictMixin, JsonMixin):
        def __init__(self, switch=None, machines=None):
            self.switch = switch
            self.machines = [
                Machine(**kwargs) for kwargs in machines]
    
    
    class Switch(ToDictMixin, JsonMixin):
        def __init__(self, ports=None, speed=None):
            self.ports = ports
            self.speed = speed
    
    
    class Machine(ToDictMixin, JsonMixin):
        def __init__(self, cores=None, ram=None, disk=None):
            self.cores = cores
            self.ram = ram
            self.disk = disk

    여기서 3개의 클래스가 등장하며, 모든 클래스는 ToDictMixin, JsonMixin 클래스를 모두 상속받는다. 

    1. ToDictMixin 클래스에서 to_dict() 메서드를 제공하고, JsonMixin 클래스는 to_dict() 메서드를 이용해 딕셔너리를 json으로 바꿀 수 있다. 
    serialized = """{
        "switch": {"ports": 5, "speed": 1e9}, 
        "machines": [
            {"cores": 8, "ram": 32e9, "disk": 5e12},
            {"cores": 4, "ram": 16e9, "disk": 1e12},
            {"cores": 2, "ram": 8e9, "disk": 500e9}
        ]
    }"""
    
    deserealized = DatacenterRack.from_json(serialized)
    roundtrip = deserealized.to_json()
    assert json.loads(serialized) == json.loads(roundtrip)

    위 코드는 DatacenterRack 클래스에 serialized라는 값을 전달하고, 그 값을 Json으로 바꾸는 것이다.

    print(roundtrip)
    >>>
    {"switch": {"ports": 5, "speed": 1000000000.0}, "machines": [{"cores": 8, "ram": 32000000000.0, "disk": 5000000000000.0}, {"cores": 4, "ram": 16000000000.0, "disk": 1000000000000.0}, {"cores": 2, "ram": 8000000000.0, "disk": 500000000000.0}]}

    코드 실행 결과를 살펴보면, DatacenterRack 클래스가 두 개의 믹스인 클래스를 상속받았고, 두 믹스인 클래스가 제공하는 기능인 to_dict(), from_json()을 잘 이용하고 있는 것을 알 수 있다. 

    댓글

    Designed by JB FACTORY