파이썬 내장 딕셔너리 타입은 객체를 동적으로 사용할 수 있도록 해줍니다.
이때 동적(Dynamic)이라는 말은 어떤 값이 들어올지 미리 알 수 없는 상태에서, 값이 들어오면 그때 그때 능동적으로 처리하는 것을 뜻합니다.
예를 들어 다음과 같은 코드가 있습니다.
학생들의 점수를 기록해야 하는데, 학생의 이름은 미리 알 수 없는 상황입니다.
이럴 때는 각 학생별로 미리 정의한 attribute를 사용하는 대신 딕셔너리에 이름을 저장하는 클래스를 정의할 수 있습니다.
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)
그러나 이 경우에는 기능을 확장할수록 점점 더 사용하기 불편할 수 있습니다.
예를 들어 학생들의 점수를 각 과목별로 저장하고, 또 각 점수의 가중치를 함께 저장해서 중간고사와 기말고사가 다른 쪽지 시험보다 성적에 더 큰 영향을 미치도록 한다고 해보죠.
from collections import defaultdict
class WeightedGradebook:
def __init__(self):
self._grades = {}
def add_student(self, name): # 빈 리스트 []를 dictionary default 값으로 설정
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_num, 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)
아까와 비교해 클래스를 쓰기 훨씬 어려워졌습니다. 특히 위치로 인자를 지정하면 어떤 값이 어떤 뜻을 가지는지 이해하기 어렵습니다.
즉 이렇게 딕셔너리, 리스트, 튜플 내부에 또 다른 계층이 포함되는 식으로 복잡도가 올라갈 경우에는 내장 타입 말고 클래스 계층 구조를 사용해야 합니다.
해당 기능을 클래스로 분리해서 데이터를 더 잘 캡슐화해주는 잘 정의된 인터페이스를 제공하는 것입니다.
이제 어떤 부분을 클래스로 리팩토링할지 고민해보죠.
먼저 의존 관계 트리의 맨 밑바닥, 즉 내포된 딕셔너리나 튜플, 리스트 등의 맨 안 쪽에 해당하는 점수를 표현하는 클래스를 고려할 수 있습니다.
그러나 이런 단순한 정보를 표현하는 클래스를 따로 만들면 너무 많은 비용이 드는 것 같기도 하고,
점수는 불변 값이므로 튜플이 더 적당해보이기도 합니다.
추가로 튜플에 저장된 내부 원소에 위치로 접근하기보다 namedtuple을 이용해보겠습니다.
원소가 3개 이상인 튜플을 사용한다면 이 편이 적절할 수 있습니다.
아래와 같이 WeightedGradebook 클래스의 인스턴스를 만들 때 위치 기반 인자 뿐만 아니라 키워드 인자를 사용할 수 있도록 하는 것입니다.
from collections import namedtuple
Grade = namedtuple('Grade', ('score', 'weight'))
이제 각 점수를 포함하는 단일 과목, 또 각 과목들을 수강하는 학생, 마지막으로 각 학생들의 정보를 저장하는 전체 Gradebook을 클래스로 표현해보겠습니다.
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(Students)
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)
gym = albert.get_subject('체육')
gym.report_grade(100, 0.40)
print(albert.average_grade())
처음 작성했던 코드보다 훨씬 읽기 쉽고 확장성도 좋아졌습니다.
아래 자료를 참고했습니다.
- 『파이썬 코딩의 기술』 Better way 37(내장 타입을 여러 단계로 내포시키기보다는 클래스를 합성하라)
'공부하며 성장하기 > 파이썬 Python' 카테고리의 다른 글
List comprehension보다 for문이 빠른 경우 (0) | 2023.06.18 |
---|---|
🏆 2021 노마드 어워즈 후기 (0) | 2021.12.30 |
[파이썬 코딩의 기술] 03/ 협업의 확장 (4) | 2021.07.05 |
[파이썬 코딩의 기술] 02/ 협업의 미덕 (0) | 2021.07.04 |
[파이썬 코딩의 기술] 01/ 협업의 기초 (0) | 2021.07.04 |