#!/usr/bin/env python
# coding: utf-8

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

# 다중 상속으로 인해 발생할 수 있는 골치 아픈 경우를 피하고 싶다면, 믹스인을 사용할지 고려해보라
# 
# 믹스인은 자식 클래스가 사용할 메서드 몇개만 정의하는 클래스다.
# 
# 믹스인 클래스에는 자체 애트리뷰트 정의가 없으므로 믹스인 클래스의 \_\_init\_\_ 메서드를 호출할 필요도 없다.

# In[2]:


class ToDictMixin:
    def to_dict(self):
        return self._traverse_dict(self.__dict__)


# 이 _traverse_dict 메서드를 hasattr을 통한 동적인 애트리뷰트 접근과 isinstance를 사용한 타입 검사, __dict__를 통한 인스턴스 딕셔너리 접근을 활용해 간단하게 구현할 수 있다.

# 이 접근 방법은 기본적인 클래스 계층의 경우에는 잘 작동하지만, 다른 경우에는 잘못될 수도 있다.
# 
# 다중 상속에 의해 영향을 받은 경우 예측할 수 없는 방식으로 작동할 수 있다.
# 
# 다중 상속을 사용하는 경우 생기는 문제 중 하나는 모든 하위 클래스에서 \_\_init\_\_호출의 순서가 정해져 있지 않다는 것이다.

# In[3]:


class ToDictMixin:
    def to_dict(self):
        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):
        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


# 다음은 이 믹스인을 사용해 이진 트리를 딕셔너리 표현으로 변환하는 예제다

# In[ ]:


class BinaryTree(ToDictMixin):
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right


# 연관된 여러 파이썬 객체들을 한 딕셔너리로 변환하는 것도 쉽게 할 수 있다.

# In[4]:


tree = BinaryTree(10,
                  left=BinaryTree(7, right=BinaryTree(9)),
                  right=BinaryTree(13, left=BinaryTree(11)))
print(tree.to_dict())


# 믹스인의 가장 큰 장점은 제너릭 기능을 쉽게 연결할 수 있고 필요할 때 기존 기능을 다른 기능으로 오버라이드해 변경할 수 있다는 것이다.

# In[5]:


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  # Prevent cycles
        else:
            return super()._traverse(key, value)


# In[6]:


root = BinaryTreeWithParent(10)
root.left = BinaryTreeWithParent(7, parent=root)
root.left.right = BinaryTreeWithParent(9, parent=root.left)
print(root.to_dict())


# In[7]:


class NamedSubTree(ToDictMixin):
    def __init__(self, name, tree_with_parent):
        self.name = name
        self.tree_with_parent = tree_with_parent

my_tree = NamedSubTree('foobar', root.left.right)
print(my_tree.to_dict()) # 무한 루프없음


# 믹스인을 서로 합성할 수도 있다.

# In[8]:


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 클래스 안에 인스턴스 메서드와 클래스 메서드가 함꼐 정의됐다는 점에 유의하라.
# 
# 믹스인을 사용하면 인스턴스의 동작이나 클래스의 동작 중 어느것이든 하위 클래스에 추가할 수 있다.
# 
# 이런 믹스인이 있으면 Json과 직렬화를 하거나 역직렬화를 할 유틸리티 클래스의 클래스 계층 구조를 쉽게, 번잡스러운 준비 코드 없이 만들 수 있다.
# 예를 들어 데이터 센터의 각 요소 간 연결을 표현하는 클래스 계층이 있다고 하자.

# In[9]:


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):
    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


# In[10]:


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}
    ]
}"""


# In[11]:


deserialized = DatacenterRack.from_json(serialized)
roundtrip = deserialized.to_json()
assert json.loads(serialized) == json.loads(roundtrip)


# ## 기억해야 할 내용
# - 믹스인을 사용해 구현할 수 있는 기능을 인스턴스 애트리뷰트와 __init__을 사용하는 다중 상속을 통해 구현하지 말라.
# - 믹스인 클래스가 클래스별로 특화된 기능을 필요로 한다면 인스턴스 수준에서 끼워 넣을 수 있는 기능(정해진 메서드를 통해 해당 기능을 인스턴스가 제공하게 만듦)을 활용하라.
# - 믹스인에는 필요에 따라 인스턴스 메서드는 물론 클래스 메서드도 포함될 수 있다.
# - 믹스인을 합성하면 단순한 동작으로부터 더 복잡한 기능을 만들어낼 수 있다.