{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 41. 기능을 합성할 때는 믹스인 클래스를 사용하라"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "다중 상속으로 인해 발생할 수 있는 골치 아픈 경우를 피하고 싶다면, 믹스인을 사용할지 고려해보라\n",
    "\n",
    "믹스인은 자식 클래스가 사용할 메서드 몇개만 정의하는 클래스다.\n",
    "\n",
    "믹스인 클래스에는 자체 애트리뷰트 정의가 없으므로 믹스인 클래스의 \\_\\_init\\_\\_ 메서드를 호출할 필요도 없다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ToDictMixin:\n",
    "    def to_dict(self):\n",
    "        return self._traverse_dict(self.__dict__)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이 _traverse_dict 메서드를 hasattr을 통한 동적인 애트리뷰트 접근과 isinstance를 사용한 타입 검사, __dict__를 통한 인스턴스 딕셔너리 접근을 활용해 간단하게 구현할 수 있다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이 접근 방법은 기본적인 클래스 계층의 경우에는 잘 작동하지만, 다른 경우에는 잘못될 수도 있다.\n",
    "\n",
    "다중 상속에 의해 영향을 받은 경우 예측할 수 없는 방식으로 작동할 수 있다.\n",
    "\n",
    "다중 상속을 사용하는 경우 생기는 문제 중 하나는 모든 하위 클래스에서 \\_\\_init\\_\\_호출의 순서가 정해져 있지 않다는 것이다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ToDictMixin:\n",
    "    def to_dict(self):\n",
    "        return self._traverse_dict(self.__dict__)\n",
    "\n",
    "    def _traverse_dict(self, instance_dict):\n",
    "        output = {}\n",
    "        for key, value in instance_dict.items():\n",
    "            output[key] = self._traverse(key, value)\n",
    "        return output\n",
    "\n",
    "    def _traverse(self, key, value):\n",
    "        if isinstance(value, ToDictMixin):\n",
    "            return value.to_dict()\n",
    "        elif isinstance(value, dict):\n",
    "            return self._traverse_dict(value)\n",
    "        elif isinstance(value, list):\n",
    "            return [self._traverse(key, i) for i in value]\n",
    "        elif hasattr(value, '__dict__'):\n",
    "            return self._traverse_dict(value.__dict__)\n",
    "        else:\n",
    "            return value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "다음은 이 믹스인을 사용해 이진 트리를 딕셔너리 표현으로 변환하는 예제다"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class BinaryTree(ToDictMixin):\n",
    "    def __init__(self, value, left=None, right=None):\n",
    "        self.value = value\n",
    "        self.left = left\n",
    "        self.right = right"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "연관된 여러 파이썬 객체들을 한 딕셔너리로 변환하는 것도 쉽게 할 수 있다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'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}}\n"
     ]
    }
   ],
   "source": [
    "tree = BinaryTree(10,\n",
    "                  left=BinaryTree(7, right=BinaryTree(9)),\n",
    "                  right=BinaryTree(13, left=BinaryTree(11)))\n",
    "print(tree.to_dict())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "믹스인의 가장 큰 장점은 제너릭 기능을 쉽게 연결할 수 있고 필요할 때 기존 기능을 다른 기능으로 오버라이드해 변경할 수 있다는 것이다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "class BinaryTreeWithParent(BinaryTree):\n",
    "    def __init__(self, value, left=None,\n",
    "                 right=None, parent=None):\n",
    "        super().__init__(value, left=left, right=right)\n",
    "        self.parent = parent\n",
    "\n",
    "    def _traverse(self, key, value):\n",
    "        if (isinstance(value, BinaryTreeWithParent) and\n",
    "                key == 'parent'):\n",
    "            return value.value  # Prevent cycles\n",
    "        else:\n",
    "            return super()._traverse(key, value)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'value': 10, 'left': {'value': 7, 'left': None, 'right': {'value': 9, 'left': None, 'right': None, 'parent': 7}, 'parent': 10}, 'right': None, 'parent': None}\n"
     ]
    }
   ],
   "source": [
    "root = BinaryTreeWithParent(10)\n",
    "root.left = BinaryTreeWithParent(7, parent=root)\n",
    "root.left.right = BinaryTreeWithParent(9, parent=root.left)\n",
    "print(root.to_dict())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'name': 'foobar', 'tree_with_parent': {'value': 9, 'left': None, 'right': None, 'parent': 7}}\n"
     ]
    }
   ],
   "source": [
    "class NamedSubTree(ToDictMixin):\n",
    "    def __init__(self, name, tree_with_parent):\n",
    "        self.name = name\n",
    "        self.tree_with_parent = tree_with_parent\n",
    "\n",
    "my_tree = NamedSubTree('foobar', root.left.right)\n",
    "print(my_tree.to_dict()) # 무한 루프없음"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "믹스인을 서로 합성할 수도 있다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "\n",
    "class JsonMixin:\n",
    "    @classmethod\n",
    "    def from_json(cls, data):\n",
    "        kwargs = json.loads(data)\n",
    "        return cls(**kwargs)\n",
    "\n",
    "    def to_json(self):\n",
    "        return json.dumps(self.to_dict())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "여기서 JsonMixin 클래스 안에 인스턴스 메서드와 클래스 메서드가 함꼐 정의됐다는 점에 유의하라.\n",
    "\n",
    "믹스인을 사용하면 인스턴스의 동작이나 클래스의 동작 중 어느것이든 하위 클래스에 추가할 수 있다.\n",
    "\n",
    "이런 믹스인이 있으면 Json과 직렬화를 하거나 역직렬화를 할 유틸리티 클래스의 클래스 계층 구조를 쉽게, 번잡스러운 준비 코드 없이 만들 수 있다.\n",
    "예를 들어 데이터 센터의 각 요소 간 연결을 표현하는 클래스 계층이 있다고 하자."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "class DatacenterRack(ToDictMixin, JsonMixin):\n",
    "    def __init__(self, switch=None, machines=None):\n",
    "        self.switch = Switch(**switch)\n",
    "        self.machines = [\n",
    "            Machine(**kwargs) for kwargs in machines]\n",
    "\n",
    "class Switch(ToDictMixin, JsonMixin):\n",
    "    def __init__(self, ports=None, speed=None):\n",
    "        self.ports = ports\n",
    "        self.speed = speed\n",
    "\n",
    "class Machine(ToDictMixin, JsonMixin):\n",
    "    def __init__(self, cores=None, ram=None, disk=None):\n",
    "        self.cores = cores\n",
    "        self.ram = ram\n",
    "        self.disk = disk"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "serialized = \"\"\"{\n",
    "    \"switch\": {\"ports\": 5, \"speed\": 1e9},\n",
    "    \"machines\": [\n",
    "        {\"cores\": 8, \"ram\": 32e9, \"disk\": 5e12},\n",
    "        {\"cores\": 4, \"ram\": 16e9, \"disk\": 1e12},\n",
    "        {\"cores\": 2, \"ram\": 4e9, \"disk\": 500e9}\n",
    "    ]\n",
    "}\"\"\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "deserialized = DatacenterRack.from_json(serialized)\n",
    "roundtrip = deserialized.to_json()\n",
    "assert json.loads(serialized) == json.loads(roundtrip)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 기억해야 할 내용\n",
    "- 믹스인을 사용해 구현할 수 있는 기능을 인스턴스 애트리뷰트와 __init__을 사용하는 다중 상속을 통해 구현하지 말라.\n",
    "- 믹스인 클래스가 클래스별로 특화된 기능을 필요로 한다면 인스턴스 수준에서 끼워 넣을 수 있는 기능(정해진 메서드를 통해 해당 기능을 인스턴스가 제공하게 만듦)을 활용하라.\n",
    "- 믹스인에는 필요에 따라 인스턴스 메서드는 물론 클래스 메서드도 포함될 수 있다.\n",
    "- 믹스인을 합성하면 단순한 동작으로부터 더 복잡한 기능을 만들어낼 수 있다."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}