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