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