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