클래스를 활용한 코드 리팩토링

 

클래스가 필요한 순간

  • dict 에 dict을 저장해야 구현 가능하다고 느끼는 순간 클래스로 나누는것이 필요하다
  • 아래 예시를 통해 어떻게 그런 순간을 맞닥뜨리는지 보자
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.add_student('철수')
book.report_grade('영희', 70)
book.report_grade('영희', 75)
book.report_grade('철수', 83)
book.average_grade('영희'), book.average_grade('철수')
(72.5, 83.0)
  • 이때 학생당 전체 성적이 아니라, 과목별 성적으로 나눠서 저장하고 싶다고 하자
    • 위 코드로 설명한다면 key ‘영희’의 value에 새로운 dict 을 추가하고, 그 dict에 각 과목을 key로 지정해서 구현해야 한다
class BySubjectGradebook:
    def __init__(self):
        self._grades = {}

    def add_student(self, name):
        self._grades[name] = {}

    def report_grade(self, name, subject, score):
        grades = self._grades[name]
        grades[subject] = score   
        
    def average_grade(self, name):
        grades = self._grades[name]
        scores = 0

        for subject, score in grades.items():
            scores += score

        return scores / len(grades)
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]        # defaultdict이 없었으면 이런식으로 append 못함
                                                # 왜? grade_list = [] 이런식으로 매번 리스트 초기화시키면 append로 여러개 값을 추가 못 시키지
                                                # 저렇게 key 주면 바로 지정한형식(여기서는 리스트)으로 바로 초기화 되게끔 해줌
        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.add_student('영희')
book.report_grade('영희', '수학', 70)
book.report_grade('영희', '수학', 764)
book.report_grade('영희', '영어', 85)
book.report_grade('철수', '체육', 151)
book.average_grade('영희'), book.average_grade('철수')
(306.3333333333333, 151.0)
  • 이 상태에서 각 과목별로 중간고사, 기말고사 점수를 입력한 후 향후 가중치로 값 변동을 위해 점수와 가중치값을 같이 묶어서 저장하고 싶다고 함
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
  • for문도 읽기 어려워지고 사용법도 위치로 인자값 넣다보니까 정신 나감
  • 클래스 분류가 필요한 시점!
# 책 보기 전 내가 먼저 구현해봄

from collections import defaultdict

# class Subject():
    

#     def __init__(self, subject, score):
#         subject, score

# Subject를 클래스로 만들지 못하겠음..
class Student():
    grade = defaultdict(list)
    # grade = {}
    name = None

    # def __init__(self, name):
    #     self.grade[name] = []
    #     self.name = name

    def add_subject(self, subject, score, weight):
        self.grade[subject].append((score, weight)) 

    def get_grade(self):
        print(self.grade)
    
    # 평균 구하는건 귀찮아서 생략!

class Gradebook():
    students = defaultdict(Student)

    def add_student(self, name):
        self.students[name] = Student()
gradebook = Gradebook()
gradebook.add_student('철수')

gradebook.students['철수'].add_subject('영어', 70, 0.3)
gradebook.students['철수'].add_subject('영어', 78, 0.7)
gradebook.students['철수'].get_grade()

# 철수 = Student()
# 철수.add_subject('수학', '70', 0.3)
# 철수.add_subject('수학', '85', 0.8)
# 철수.get_grade()
defaultdict(<class 'list'>, {'영어': [(70, 0.3), (78, 0.7)]})
from collections import 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]
  • Subject에 여러가지 점수(수행평가, 중간고사, 기말고사 등)가 들어올 수 있으니 list로 관리한다
    • 그리고 여러 클래스들이 공유하게끔 하지 않고 인스턴스 하나만 사용할 수 있는 저장공간을 만들기 위해 self로 리스트를 초기화하는 모습을 볼 수 있다.
  • Student 클래스는 여러 Subject 과목들을 관리하기 위해 Subject를 value로 받는 dictionary를 사용한다
  • Gradebook 클래스는 여러 Student 학생들을 관리하기 위해 Student를 value로 받는 dictionary를 사용한다

위 피드백 코드를 잠깐 훑고 바로 다시 구현해보기

from collections import defaultdict

class Subject():
    def __init__(self, name):
        self.grade = defaultdict(list)
        self.subj_name = name
        self.scores = 0
        self.total_weight = 0

    def report_grade(self, score, weight):
        self.grade[self.subj_name].append((score, weight))     
    
    def get_average_score(self):
        scores = 0
        # for dic_list in self.grade.values():    # value에 list를 넣었다고 해서 dictionary.values()가 list 타입으로 반환되는게 아니였음
        for score, weight in list(self.grade.values())[0]:
            scores += score
            self.total_weight += weight
        ave = scores / len(list(self.grade.values())[0])

        self.scores = 0
        self.total_weight = 0       

        return ave

class Student():
    def __init__(self, stud_name):
        self.grades = defaultdict(Subject)
        self.stud_name = stud_name

    def add_subject(self, subject:Subject):
        self.grades[subject.subj_name] = subject

    def get_average(self):
        scores = 0

        for subject_name, subject in self.grades.items():
            scores += subject.get_average_score()      
        
        return scores / len(self.grades)
  • 과목명, 가중치등이 어떻게 Subject class에 저장해야 할 지 고민했음
    • 과목명은 포함되어야 할 것 같았지만, 가중치는 다른 과목과 연관되는 것이니 제외하려고 생각했었음
      • 하지만 가중치가 결국 score와 1:1로 매칭되어야 하는 값이라고 최종 판단해서 저장하게 함
  • score가 어떤 자료형으로 Subject class에 저장해야 할 지 고민했음
    • 중간고사, 기말고사, 수행평가등 같은 과목이여도 여러 점수가 들어갈 수 있다는 생각에 list형태로 저장할 수 있는 defautdict(list) 사용
      • dict.values를 통해서 list 타입이 반환되지 않는것을 깨달음.. list(self.grade.values())[0] 와 같이 코드가 해괴망측해짐
sub1 = Subject('과학')
sub1.report_grade(65, 0.3)
sub1.report_grade(72, 0.6)
sub2 = Subject('영어')
sub2.report_grade(45, 0.5)
sub2.report_grade(81, 0.7)
sub2.report_grade(77, 0.3)

print(f'{sub1.subj_name} 평균: {sub1.get_average_score()}, {sub2.subj_name} 평균: {sub2.get_average_score()}')

stu1 = Student('철수')
stu1.add_subject(sub1)
stu1.add_subject(sub2)

print(f'{stu1.stud_name} 평균: {stu1.get_average()}')
print(f'{stu1.stud_name} 평균: {stu1.get_average()}')
과학 평균: 68.5, 영어 평균: 67.66666666666667
철수 평균: 68.08333333333334
철수 평균: 68.08333333333334

최종적으로 다시 답안 코드 보기

from collections import namedtuple, defaultdict

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]
  • 과목 이름을 Subject 클래스에서 따로 저장하지 않고 있는 모습을 볼 수 있음
    • 과목 이름은 다른 과목들이 있을 때 구분 짓기 위한 것이기 때문에 과목 하나에 집중하는 Subject 클래스에서 따로 저장할 필요가 없음!
  • Student 클래스에서는 이미 Subject 클래스에서 구현된 average_grade를 활용해 쉽게 average를 구하고 있는것을 확인 가능
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
  • get_subject 등으로 Student class에 Subject class를 저장하는게 어색해서 내 방식대로 구현해보자
class Subject():
    def __init__ (self):
        self.grades = []
    
    def report_grade(self, score, weight):
        self.grades.append((score, weight))
    
    def get_average_score(self):
        total_score = 0
        total_weight = 0

        for score, weight in self.grades:
            total_score += score
            total_weight += weight
        
        return total_score / len(self.grades)

class Student():
    def __init__(self):
        self.subjects = defaultdict(Subject)
    
    def add_subject(self, name, subject:Subject):
        self.subjects[name] = subject
    
    def get_average_score(self):
        scores = 0
        for name, subject in  self.subjects.items():    # 이거 self.subjects.value() for문 적용해도 잘 되나? 될 것 같음.
            print("출력")
            print(subject.get_average_score())
            scores += subject.get_average_score()
        
        return scores / len(self.subjects)
sub1 = Subject()
sub2 = Subject()
sub3 = Subject()

sub1.report_grade(60, 0.3)
sub1.report_grade(75, 0.8)
sub1.report_grade(92, 0.7)
sub2.report_grade(60, 0.3)
sub2.report_grade(13, 0.3)
sub3.report_grade(45, 0.3)
sub3.report_grade(70, 0.3)

stu1 = Student()
stu1.add_subject('수학', sub1)
stu1.add_subject('과학', sub2)
stu1.add_subject('영어', sub3)

stu1.get_average_score()
출력
75.66666666666667
출력
36.5
출력
57.5





56.555555555555564
  • 교재 방식과 내 방식에서 객체를 쓰는 방법이 조금 차이가 있다. 각 방식의 장점 혹은 단점이 존재할까? 계속 공부하면서 알아보자