파이썬 내장 딕셔너리 타입을 사용하면 객체의 생명 주기 동안 동적인 내부 상태를 잘 유지할 수 있습니다.
--> 동적(dynamic)이라는 말은 어떤 값이 들어올지 미리 알 수 없는 식별자들을 유지해야 한다는 뜻
(예) 학생들의 점수를 기록해야 하는데, 학생의 이름은 미리 알 수 없는 상황이라고 가정
--> 학생별로 미리 정의된 애트리뷰트를 사용하는 대신 딕셔너리에 이름을 저장하는 클래스를 정의
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
- 딕셔너리와 관련 내장 타입은 사용하기 쉬우므로 과하게 확장하면서 깨지기 쉬운 코드를 작성할 위험성이 있습니다.
- 아래 코드는 내부 딕셔너리로 defaultdict의 인스턴스를 사용해서 과목이 없는 경우를 처리합니다.
(예) SimpleGradebook 클래스를 확장해서 전체 성적이 아니라 과목별 성적을 리스트로 저장하고 싶다고 할 때.
from collections import defaultdict
class BySubjectGradebook:
def __init__(self):
self._grades = {} # 외부 # dict
def add_student(self, name):
self._grades[name] = defaultdict(list) # 내부 # dict
다단계 딕셔너리를 처리해야 하므로 report_grade와 average_grade 메서드가 많이 복잡해지지만,
아직은 충분히 복잡도를 관리할 수 있을 것 같습니다.
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))
위의 코드에서 변경된 부부은 성적 리스트가 튜플 인스턴스를 저장하게 했다는것
하지만 변경된 average_grade 메서드는 루프 안에 루프가 쓰이면서 더 읽기 어려워졌습니다.
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
retrun score_sum / score_count
위치로 인자를 지정하면 어떤 값이 어떤 뜻을 가지는지 이해하기 어려우므로 클래스도 쓰기 어렵습니다. (아래 코드 참고)
book = WeightedGradebook()
book.add_student('알버트 아인슈타인')
book.report_grade('알버트 아인슈타인', '수학', 75, 0.05)
book.report_grade('알버트 아인슈타인', '수학', 65, 0.15)
book.report_grade('알버트 아인슈타인', '수학', 75, 0.80)
book.report_grade('알버트 아인슈타인', '수학', 75, 0.40)
book.report_grade('알버트 아인슈타인', '수학', 75, 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
- 원소가 3개 이상인 튜플을 사용한다면 다른 접근 방법을 생각해보자.
(해결방법) collection 내장 모듈에 있는 namedtuple 타입이 이런 경우에 딱 들어맞습니다.
namedtuple을 사용하면 작은 불변 데이터 클래스를 쉽게 정의할 수 있습니다.
from collections import namedtuple
Grade = namedtuple('Grade', ('score', 'weight'))
이 클래스의 인스턴스를 만들 때는 위치 기반 인자를 사용해도 되고 키워드 인자를 사용해도 됩니다.
요구사항이 바뀌는 경우, namedtuple을 클래스로 변경하기도 쉽습니다.
- (예) 가변성을 지원해야하거나, 간단한 데이터 컨테이너 이상의 동작이 필요한 경우
- namedtuple을 쉽게 클래스로 바꿀 수 있다.
namedtuple의 한계
1) namedtuple 클래스에는 디폴트 인자 값을 지정할 수 없다.
2) 외부에 제공하는 API의 경우 이런 특성으로 인해 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.005)
math.report_grade(65, 0.15)
math.report_grade(70, 0.80)
gym = albert.get_subject('체육')
math.report_grade(100, 0.40)
math.report_grade(85, 0.60)
print(albert.average_grade())
>>>
80.25
또한, 하위 호환성을 제공하는 메서드를 작성해서
- 예전 스타일의 API를 사용 중인 코드를 새로운 객체 계층을 사용하는 코드로 쉽게 마이그레이션할 수도 있습니다.
### 기억해야 할 내용
- 딕셔너리, 긴 튜플, 다른 내장 타입이 복잡하게 내포된 데이터를 값으로 사용하는 딕셔너리를 만들지 말라.
- 완전한 클래스가 제공하는 유연성이 필요하지 않고 가벼운불변 데이터 컨테이너가 필요하다면 namedtuple을 사용하라.
- 내부 상태를 표현하는 딕셔너리가 복잡해지면 이 데이터를 관리하는 코드를 여러 클래스로 나눠서 작성하라.
'파이썬 코딩의 기술' 카테고리의 다른 글
파이썬코딩의 기술 - BW50 : __set_name__으로 클래스 애트리뷰트를 표시하라 (0) | 2023.03.17 |
---|---|
파이썬코딩의 기술 - BW38 : 간단한 인터페이스의 경우 클래스 대신 함수를 받아라 (0) | 2023.03.08 |
파이썬코딩의기술 - BW36 : 이터레이터나 제너레이터를 다룰 때는 itertools를 사용하라 (0) | 2023.02.28 |
파이썬 코딩의 기술 - BW35 : 제너레이터 안에서 throw 상태를 변화시키지 마라 (0) | 2023.02.28 |
파이썬코딩의기술 - BW16 in을 사용하고 딕셔너리 키가 없을때 KeyError를 처리하기보다는 get을 사용하라 (0) | 2023.02.22 |