{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 37. 내장 타입을 여러 단계로 내포시키보다는 클래스를 합성하라"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "파이썬 내장 딕셔너리 타입을 사용하면 객체의 생명 주기 동안 동적인 내부 상태를 잘 유지할 수 있다.\n",
    "\n",
    "여기서 동적이라는 말은 어떤 값이 들어올지 미리 알 수 없는 식별자들을 잘 유지해야 한다는 뜻이다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "class SimpleGradebook:\n",
    "    def __init__(self):\n",
    "        self._grades = {}\n",
    "\n",
    "    def add_student(self, name):\n",
    "        self._grades[name] = []\n",
    "\n",
    "    def report_grade(self, name, score):\n",
    "        self._grades[name].append(score)\n",
    "\n",
    "    def average_grade(self, name):\n",
    "        grades = self._grades[name]\n",
    "        return sum(grades) / len(grades)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "90.0\n"
     ]
    }
   ],
   "source": [
    "book = SimpleGradebook()\n",
    "book.add_student('아이작 뉴턴')\n",
    "book.report_grade('아이작 뉴턴', 90)\n",
    "book.report_grade('아이작 뉴턴', 95)\n",
    "book.report_grade('아이작 뉴턴', 85)\n",
    "\n",
    "print(book.average_grade('아이작 뉴턴'))"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "딕셔너리 관련 내장 타입은 사용하기 너무 쉬우므로 과하게 확장하면서 깨지기 쉬운 코드를 작성할 위험성이 있다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import defaultdict"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "class BySubjectGradebook:\n",
    "    def __init__(self):\n",
    "        self._grades = {}  # 외부 dict\n",
    "\n",
    "    def add_student(self, name):\n",
    "        self._grades[name] = defaultdict(list)  # 내부 dict\n",
    "\n",
    "    def report_grade(self, name, subject, grade):\n",
    "        by_subject = self._grades[name]\n",
    "        grade_list = by_subject[subject]\n",
    "        grade_list.append(grade)\n",
    "\n",
    "    def average_grade(self, name):\n",
    "        by_subject = self._grades[name]\n",
    "        total, count = 0, 0\n",
    "        for grades in by_subject.values():\n",
    "            total += sum(grades)\n",
    "            count += len(grades)\n",
    "        return total / count"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "81.25\n"
     ]
    }
   ],
   "source": [
    "book = BySubjectGradebook()\n",
    "book.add_student('알버트 아인슈타인')\n",
    "book.report_grade('알버트 아인슈타인', '수학', 75)\n",
    "book.report_grade('알버트 아인슈타인', '수학', 65)\n",
    "book.report_grade('알버트 아인슈타인', '체육', 90)\n",
    "book.report_grade('알버트 아인슈타인', '체육', 95)\n",
    "print(book.average_grade('알버트 아인슈타인'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이 코드는 아주 평이하다.\n",
    "\n",
    "아직은 충분히 복잡도를 관리 할 수 있다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이제 요구 사항이 바뀐다.\n",
    "\n",
    "각 점수의 가중치를 함꼐 저장해서 중간고사와 기말고사가 다른 쪽지 시험보다 더 큰 영향을 미치게 하고 싶다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "class WeightedGradebook:\n",
    "    def __init__(self):\n",
    "        self._grades = {}\n",
    "\n",
    "    def add_student(self, name):\n",
    "        self._grades[name] = defaultdict(list)\n",
    "\n",
    "    def report_grade(self, name, subject, score, weight):\n",
    "        by_subject = self._grades[name]\n",
    "        grade_list = by_subject[subject]\n",
    "        grade_list.append((score, weight))\n",
    "\n",
    "    def average_grade(self, name):\n",
    "        by_subject = self._grades[name]\n",
    "        score_sum, score_count = 0, 0\n",
    "\n",
    "        for subject, scores in by_subject.items():\n",
    "            subject_avg, total_weight = 0, 0\n",
    "\n",
    "            for score, weight in scores:\n",
    "                subject_avg += score * weight\n",
    "                total_weight += weight\n",
    "\n",
    "            score_sum += subject_avg / total_weight\n",
    "            score_count += 1\n",
    "\n",
    "        return score_sum / score_count"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "80.25\n"
     ]
    }
   ],
   "source": [
    "book = WeightedGradebook()\n",
    "book.add_student('알버트 아인슈타인')\n",
    "book.report_grade('알버트 아인슈타인', '수학', 75, 0.05)\n",
    "book.report_grade('알버트 아인슈타인', '수학', 65, 0.15)\n",
    "book.report_grade('알버트 아인슈타인', '수학', 70, 0.80)\n",
    "book.report_grade('알버트 아인슈타인', '체육', 100, 0.40)\n",
    "book.report_grade('알버트 아인슈타인', '체육', 85, 0.60)\n",
    "print(book.average_grade('알버트 아인슈타인'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "클래스도 쓰기 어려워졌다.\n",
    "\n",
    "위치로 인자를 지정하면 어떤 값이 어떤 뜻을 가지는지 이해하기 어렵다.\n",
    "\n",
    "이와 같은 복잡도가 눈에 들어오면 더 이상 딕셔너리, 튜플, 집합, 리스트 등의 내장 타입을 사용하지 말고 클래스 계층 구조를 사용해야 한다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 클래스를 활용해 리팩터링하기\n",
    "\n",
    "리팩터링할 떄 취할 수 있는 접근 방법은 많다.\n",
    "\n",
    "여기서는 먼저 의존 관계 트리의 맨 밑바닥을 점수를 표현하는 클래스로 옮겨갈 수 있다.\n",
    "\n",
    "하지만 이런 단순한 정보를 표현하는 클래스를 따로 만들면 너무 많은 비용이 드는 것 같다.\n",
    "\n",
    "게다가 점수는 불변 값이기 때문에 튜플이더 적당해 보인다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "grades = []\n",
    "grades.append((95, 0.45))\n",
    "grades.append((85, 0.55))\n",
    "total = sum(score * weight for score, weight in grades)\n",
    "total_weight = sum(weight for _, weight in grades)\n",
    "average_grade = total / total_weight"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이 코드의 문제점은 튜플에 저장된 내부 원소에 위치를 사용해 접근한다는 점이다.\n",
    "\n",
    "원소가 늘어나면 다른 인덱스를 무시하기 위해 _ 를 더 많이 써야한다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "grades = []\n",
    "grades.append((95, 0.45, '참 잘했어요'))\n",
    "grades.append((85, 0.55, '조금 만 더 열심히'))\n",
    "total = sum(score * weight for score, weight, _ in grades)\n",
    "total_weight = sum(weight for _, weight, _ in grades)\n",
    "average_grade = total / total_weight"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "원소가 세 개 이상인 튜플을 사용한다면 다른 접근 방법을 생각해봐야 한다.\n",
    "\n",
    "collection 내장 모듈에 있는 namedtuple 타입이 이런 경우에 딱 들어 맞는다.\n",
    "\n",
    "namedtuple을 사용하면 작은 불변 데이터 클래스를 쉽게 정의할 수 있다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import namedtuple"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "Grade = namedtuple('Grade', ('score', 'weight'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이 클래스의 인스턴스를 만들 때는 위치 기반 인자를 사용해도 되고 키워드 인자를 사용해도 된다.\n",
    "\n",
    "필드에 접근할 때는 애트리뷰트 이름을 쓸 수 있다.\n",
    "\n",
    "이름이 붙은 애트리뷰트를 사용할 수 있으므로 요구사항이 바뀌는 경우에 namedtuple을 클래스로 변경하기도 쉽다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### namedtuple의 한계\n",
    "\n",
    "namedtuple이 유용한 상황이 많지만, 득보다 실이 많은 경우도 있다는 사실을 잊지 말아야 한다.\n",
    "\n",
    "- namedtuple 클래스에는 디폴트 인자 값을 지정할 수 없다. 따라서 선택적인 프로퍼티가 많은 데이터에 namedtuple을 사용하기는 어렵다. 프로퍼티가 4~5개보다 더 많아지면 dataclasses 내장 모듈을 사용하는 편이 낫다.\n",
    "- 여전히 namedtuple 인스턴스의 애트리뷰트 값을 숫자 인덱스를 사용해 접근할 수 있고 이터레이션도 가능하다. 특히 외부에 제공하는 API의 경우 이런 특성으로 인해 나중에 namedtuple을 실제 클래스로 변경하기 어려울 수도 있다. 여러분이 namedtuple을 사용하는 모든 부분을제어할 수 있는 상황이 아니라면 명시적으로 새로운 클래스를 정의하는 편이 더 낫다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 일련의 점수를 포함하는 단일 과목을 표현하는 클래스\n",
    "\n",
    "class Subject:\n",
    "    def __init__(self):\n",
    "        self._grades = []\n",
    "\n",
    "    def report_grade(self, score, weight):\n",
    "        self._grades.append(Grade(score, weight))\n",
    "\n",
    "    def average_grade(self):\n",
    "        total, total_weight = 0, 0\n",
    "        for grade in self._grades:\n",
    "            total += grade.score * grade.weight\n",
    "            total_weight += grade.weight\n",
    "        return total / total_weight"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 한 학생이 수강하는 과목들을 표현하는 클래스\n",
    "\n",
    "class Student:\n",
    "    def __init__(self):\n",
    "        self._subjects = defaultdict(Subject)\n",
    "\n",
    "    def get_subject(self, name):\n",
    "        return self._subjects[name]\n",
    "\n",
    "    def average_grade(self):\n",
    "        total, count = 0, 0\n",
    "        for subject in self._subjects.values():\n",
    "            total += subject.average_grade()\n",
    "            count += 1\n",
    "        return total / count"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 모든 학생을 저장하는 컨테이너\n",
    "\n",
    "class Gradebook:\n",
    "    def __init__(self):\n",
    "        self._students = defaultdict(Student)\n",
    "\n",
    "    def get_student(self, name):\n",
    "        return self._students[name]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "80.25\n"
     ]
    }
   ],
   "source": [
    "book = Gradebook()\n",
    "albert = book.get_student('알버트 아인슈타인')\n",
    "math = albert.get_subject('수학')\n",
    "math.report_grade(75, 0.05)\n",
    "math.report_grade(65, 0.15)\n",
    "math.report_grade(70, 0.80)\n",
    "gym = albert.get_subject('체육')\n",
    "gym.report_grade(100, 0.40)\n",
    "gym.report_grade(85, 0.60)\n",
    "print(albert.average_grade())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 기억해야 할 내용\n",
    "- 딕셔너리, 긴 튜플, 다른 내장 타입이 복잡하게 내포된 데이터를 값으로 사용하는 딕셔너리를 만들지 마라.\n",
    "- 완전한 클래스가 제공하는 유연성이 필요하지 않고  가벼운 불변 데이터 컨테이너가 필요한다면 namedtuple을 사용하라.\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
}