{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## 39. 객체를 제너릭하게 꾸성하려면 @classmethod를 통한 다형성을 활용하라" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "파이썬에서는 객체뿐만 아니라 클래스도 다형성을 지원한다.\n", "\n", "다형성을 사용하면 계층을 이루는 여러 클래스가 자신에게 맞는 유일한 메서드 버전을 구현할 수 있다.\n", "\n", "이는 같은 인터페이스를 만족하거나 같은 추상 기반 클래스를 공유하는 많은 클래스가 서로 다른 기능을 제공할 수 있다는 뜻이다.\n", "\n", "예를 들어 맵리듀스 구현을 하나 작성하는데 입력 데이터를 표현할 수 있는 공통 클래스가 필요하다가 하자.\n", "\n", "다음 코드는 이럴 때 사용하기 위해 정의한, 하위 클래스에서 다시 정의해야만 하는 read 메서드가 들어 있는 공통 클래스를 보여준다." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "class InputData:\n", " def read(self):\n", " raise NotImplementedError" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class PathInputData(InputData):\n", " def __init__(self, path):\n", " super().__init__()\n", " self.path = path\n", " \n", " def read(self):\n", " with open(self.path) as f:\n", " return f.read()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "하위 클래스를 만들 수 있고, 하위 클래스는 처리할 데이터를 돌려주는 공통 read 인터페이스를 구현해야 한다.\n", "\n", "비슷한 방법으로, 이 입력 데이터를 소비하는 공통 방법을 제공하는 맵 리듀스 작업자로 쓸 수 있는 추상 인터페이스를 정의하고 싶다." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class Worker:\n", " def __init__(self, input_data):\n", " self.input_data = input_data\n", " self.result = None\n", " \n", " def map(self):\n", " raise NotImplementedError\n", " \n", " def reduce(self):\n", " raise NotImplementedError" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "다음 코드는 하위 클래스다." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class LineCountWorker(Worker):\n", " def map(self):\n", " data = self.input_data.read()\n", " self.result = data.count('\\n')\n", " \n", " def reduce(self, other):\n", " self.result += other.result" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "이 구현은 아주 잘 작동할 것처럼 보이지만 각 부분을 연결하는 것이 문제다.\n", "\n", "인터페이스와 추상화를 제공하는 멋진 클래스를 만들었지만 객체를 생성해 활용해야만 모든 클래스가 쓸모 있다.\n", "\n", "가장 간단한 방법은 도우미 함수를 활용해 객체를 만들고 연결한다." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "def generate_inputs(data_dir):\n", " for name in os.listdir(data_dir):\n", " yield PathInputData(os.path.join(data_dir, name))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "다음으로 방금 generate_inputs를 통해 만드는 InputData 인스턴스들을 사용하는 LineCountWorker 인스턴스를 만든다." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def create_workers(input_list):\n", " workers = []\n", " for input_data in input_list:\n", " workers.append(LineCountWorker(input_data))\n", " return workers" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "이 Worker 인스턴스의 map 단계를 여러 스레드에 공급해서 실행할 수 있다.\n", "\n", "그 후 reduce를 반복적으로 호출해서 결과를 최종 값으로 합칠 수 있다." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "from threading import Thread\n", "\n", "\n", "def execute(workers):\n", " threads = [Thread(target=w.map) for w in workers]\n", " for thread in threads: thread.start()\n", " for thread in threads: thread.join()\n", "\n", " first, *rest = workers\n", " for worker in rest:\n", " first.reduce(worker)\n", " return first.result\n", "\n", "def mapreduce(data_dir):\n", " inputs = generate_inputs(data_dir)\n", " workers = create_workers(inputs)\n", " return execute(workers)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "몇 가지 입력 파일을 대상으로 이 함수를 실행해보면 아주 훌륭하게 작동한다." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "총 4818 줄이 있습니다.\n" ] } ], "source": [ "import os\n", "import random\n", "\n", "def write_test_files(tmpdir):\n", " os.makedirs(tmpdir)\n", " for i in range(100):\n", " with open(os.path.join(tmpdir, str(i)), 'w') as f:\n", " f.write('\\n' * random.randint(0, 100))\n", "\n", "tmpdir = 'test_inputs'\n", "write_test_files(tmpdir)\n", "\n", "result = mapreduce(tmpdir)\n", "print(f'총 {result} 줄이 있습니다.')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "하지만 문제가 존재한다. 함수가 전혀 제너릭을 사용하지 않는다.\n", "\n", "다른 InputData나 Worker 하위 클래스를 사용하고 싶다면 각 하위 클래스에 맞게 generate_inputs, create_workers, mapreduce를 재작성해야 한다.\n", "\n", "파이썬에서는 생성자 메서드가 \\_\\_init\\_\\_ 밖에 없다는게 문제다.\n", "\n", "이 ㅁ누제를 해결하는 가장 좋은 방법은 클래스 메서드 다형성을 사용하는 것이다.\n", "\n", "이 방식은 InputData.read에서 사용했던 인스턴스 메서드의 다형성과 똑같은데, 클래스로 만들어낸 개별 객체에 적용되지 않고 클래스 전체에 적용된다는 점만 다르다." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**특히 cls를 사용하면 메서드 안에서 현재 클래스의 인스턴스를 만들 수도 있다. 즉, cls는 클래스이므로 cls()는 PathInputData가 된다.** " ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "class GenericInputData:\n", " def read(self):\n", " raise NotImplementedError\n", "\n", " @classmethod\n", " def generate_inputs(cls, config):\n", " raise NotImplementedError" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "class PathInputData(GenericInputData):\n", " def __init__(self, path):\n", " super().__init__()\n", " self.path = path\n", "\n", " def read(self):\n", " with open(self.path) as f:\n", " return f.read()\n", "\n", " @classmethod\n", " def generate_inputs(cls, config):\n", " data_dir = config['data_dir']\n", " for name in os.listdir(data_dir):\n", " yield cls(os.path.join(data_dir, name))" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "class GenericWorker:\n", " def __init__(self, input_data):\n", " self.input_data = input_data\n", " self.result = None\n", "\n", " def map(self):\n", " raise NotImplementedError\n", "\n", " def reduce(self, other):\n", " raise NotImplementedError\n", "\n", " @classmethod\n", " def create_workers(cls, input_class, config):\n", " workers = []\n", " for input_data in input_class.generate_inputs(config):\n", " workers.append(cls(input_data))\n", " return workers" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "이 코드에서 input_class.generate_inputs 호출이 바로 여기서 보여주려는 클래스 다형성의 예이다." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "class LineCountWorker(GenericWorker):\n", " def map(self):\n", " data = self.input_data.read()\n", " self.result = data.count('\\n')\n", "\n", " def reduce(self, other):\n", " self.result += other.result" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "def mapreduce(worker_class, input_class, config):\n", " workers = worker_class.create_workers(input_class, config)\n", " return execute(workers)" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": true, "jupyter": { "outputs_hidden": true }, "tags": [] }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Exception in thread Thread-176:\n", "Traceback (most recent call last):\n", " File \"/Users/Danny/.pyenv/versions/3.8.5/lib/python3.8/threading.py\", line 932, in _bootstrap_inner\n", " self.run()\n", " File \"/Users/Danny/.pyenv/versions/3.8.5/lib/python3.8/threading.py\", line 870, in run\n", " self._target(*self._args, **self._kwargs)\n", " File \"<ipython-input-12-14df73dd467d>\", line 3, in map\n", " File \"<ipython-input-10-0e33911915a1>\", line 7, in read\n", "IsADirectoryError: [Errno 21] Is a directory: 'test_inputs/.ipynb_checkpoints'\n" ] }, { "ename": "TypeError", "evalue": "unsupported operand type(s) for +=: 'int' and 'NoneType'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m<ipython-input-14-dd7885461a3c>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mconfig\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m'data_dir'\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mtmpdir\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmapreduce\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mLineCountWorker\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mPathInputData\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconfig\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf'총 {result} 줄이 있습니다.'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m<ipython-input-13-7a9ba951abae>\u001b[0m in \u001b[0;36mmapreduce\u001b[0;34m(worker_class, input_class, config)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mmapreduce\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mworker_class\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minput_class\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconfig\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0mworkers\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mworker_class\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcreate_workers\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minput_class\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconfig\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mexecute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mworkers\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m<ipython-input-7-61b32e0e71f4>\u001b[0m in \u001b[0;36mexecute\u001b[0;34m(workers)\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0mfirst\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0mrest\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mworkers\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mworker\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrest\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 11\u001b[0;31m \u001b[0mfirst\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreduce\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mworker\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 12\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mfirst\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mresult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m<ipython-input-12-14df73dd467d>\u001b[0m in \u001b[0;36mreduce\u001b[0;34m(self, other)\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mreduce\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mother\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 7\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mresult\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0mother\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mresult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for +=: 'int' and 'NoneType'" ] } ], "source": [ "config = {'data_dir': tmpdir}\n", "result = mapreduce(LineCountWorker, PathInputData, config)\n", "print(f'총 {result} 줄이 있습니다.')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 기억해야 할 내용\n", "- 파이썬의 클래스에는 생성자가 \\_\\_init\\_\\_ 메서드 뿐이다.\n", "- @classmethod를 사용하면 클래스에 다른 생성자를 정의할 수 있다.\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 }