[BW 37] 내장 타입을 여러 단계로 내포시키기보다는 클래스를 합성하라

클래스와 상속을 사용하는 방법을 잘 알아두면 유지 보수하기 쉬운 코드를 작성할 수 있다.

 

파이썬 내장 딕셔너리 타입을 사용하면 객체의 생명 주기 동안 동적인 내부 상태를 잘 유지할 수 있다.

동적이라는 말은 어떤 값이 들어올지 미리 알 수 없는 식별자들을 유지해야 한다는 뜻이다.

예를 들어 학생들의 점수를 기록해야 하는데, 이름은 미리 알 수 없는 상황이라고 하자.

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

이 클래스는 쉽게 사용할 수 있지만, 딕셔너리와 관련된 내장 타입은 사용하기 쉬우므로 과하게 확장하면서 깨지기 쉬운 코드를 작성할 위험성이 있다.

 

예를 들어 SimpleGradeBook 클래스를 확장해서 전체 성적이 아니라 과목별 성적을 리스트로 저장한다고 하자.

from collections import defaultdict

class SimpleGradeBook:
    def __init__(self):
        self._grades = {}
    
    def add_student(self, name):
        self._grades[name] = defaultdict(list)
    
    def report_grade(self, name, subject, score):
        by_subject = self._grades[name]
        grade_list = by_subject[subject]
        grade_list.append(score)
    
    def average_grade(self, name):
        by_subject = self._grades[name]
        total, count = 0, 0
        for scores in by_subject.values():
            total += sum(scores)
            count += len(scores)
        return total / count

book = SimpleGradeBook()
book.add_student('홍길동')
book.report_grade('홍길동', '수학', 90)
book.report_grade('홍길동', '수학', 10)
book.report_grade('홍길동', '영어', 95)
book.report_grade('홍길동', '영어', 70)
book.report_grade('홍길동', '국어', 85)
book.report_grade('홍길동', '국어', 40)

print(book.average_grade('홍길동'))

>>>
65.0

아직까지는 충분히 복잡도를 관리할 수 있을 거 같다.

 

하지만 각 점수의 가중치를 함께 저장한다면 어떨까?

def report_grade(self, name, subject, score, weight):
    by_subject = self._grades[name]
    grade_list = by_subject[subject]
    grade_list.append((score, weight))

이때부터 클래스도 쓰기 어려워지고, 어떤 값이 어떤 뜻을 가지는지 이해하기 어렵다.

 

이럴 때 기능을 클래스로 분리하면 비록 코드는 길어지지만, 더 읽기 쉽고 유지보수하기 쉬운 코드가 완성된다.

from collections import defaultdict, namedtuple

Grade = namedtuple('Grade', ('score', 'weight'))

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()
hong = book.get_student('홍길동')
math = hong.get_subject('수학')
math.report_grade(75, 0.05)
math.report_grade(65, 0.15)
math.report_grade(70, 0.80)
gym = hong.get_subject('체육')
gym.report_grade(100, 0.40)
gym.report_grade(85, 0.60)
print(hong.average_grade())

>>>
80.25

namedtuple을 사용하면 작은 불변 데이터 클래스를 쉽게 정의할 수 있다.