티스토리 뷰

728x90

 

 

객체지향 언어로서 파이썬은 상속(inheritance), 다형성(polymorphism), 캡슐화(encapsultion) 등과 같은 기능을 제공한다. 파이썬으로 원하는 작업을 수행하기 위해 새로운 클래스를 작성하고, 새로 작성한 클래스들이 인터페이스와 계층 구조를 통해 상호작용하는 방식을 정의해야 한다.

 

파이썬 내장 딕셔너리 타입을 사용하면 객체 생명 주기 동안 동적인 내부 상태를 유지할 수 있다. 동적(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

 

예를 들어, 과목별 성적을 리스트로 저장하고 싶다고 하면, _grades 딕셔너리를 변경해서 학생 이름(키)이 아닌 다른 딕셔너리(값)으로 매핑하게 하고, 이 딕셔너리가 다시 과목(키)을 성적의 리스트(값)에 매핑하게 함으로써 과목별 성적을 구현할 수 있다.

 

다음 코드는 내부 딕셔너리로 defaultdict의 인스턴스를 사용해서 과목이 없는 경우를 처리한다(

 

from collections import defaultdict

class BySubjectGradebook:
	def __init__(self):
    		self._Grades = {} # 외부 dict
        
  	def add_student(self, name):
    		self._grades[name] = defaultdict(list) # 내부 dict

 

다단계 딕셔너리를 처리해야 하므로 메서드가 많이 복잡해지지만, 복잡도를 관리할 수 있다.

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('알버트 아인슈타인'))

 

다시 요구 사항을 더 추가하여, 각 점수의 가중치를 함꼐 저장해서 중간고사와 기말고사가 다른 쪽지 시험보다 성적에 더 큰 영향을 미치게 하고 싶다.

 

이런 기능을 구현하는 한 가지 방법은 가장 안쪽에 있는 딕셔너리가 과목을 성적의 리스트로 매핑하던 것을 (성적, 가중치) 튜플의 리스트로 매핑하도록 변경하는 것이다.

 

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('알버트 아인슈타인'))

report_grade의 메서드는 리스트가 아닌 튜플 인스턴스가 들어가도록 변경되었다. 하지만 변경된 average_grade 메서드는 루프 안에 루프가 쓰이면서 복잡해졌다.

 

이렇게 여러 내장 타입을 사용하여 구현하게 되면, 클래스를 사용 시에 의미를 한 눈에 파악하기 힘들고 어떤 인자를 어떤 위치에 넣어야 하는지 헷갈리게 된다.

 

이럴 때 클래스 계층 구조를 사용해야 한다. 내포 단계가 두 단계 이상이 되면 더 이상 딕셔너리, 리스트, 튜플 계층을 사용하지 않아야 하고, 유지 보수가 어렵다. 코드에서 값을 관리하는 부분이 복잡해진다면 해당 기능을 클래스로 분리하는 것이 좋다. 데이터를 더 잘 캡슐화 해주는 잘 정의된 인터페이스를 제공하는 것이다.

 

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'))

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())

 

이렇게 되면 결론적으로 학생 이름을 먼저 저장한 다음, 과목, 점수, 가중치 정보를 저장할 수 있다. 새로운 정보에 해당하는 학생 > 과목 > (점수, 가중치) 정보를 계층적으로 사용하는 구조가 완성된 것이다.

 

  • 딕셔너리, 긴 튜플, 다른 내장 타입이 복잡하게 내포된 데이터를 값으로 사용하는 딕셔너리를 만들지 말 것
  • 완전한 클래스가 제공하는 유연성이 필요하지 않고 가벼운 불변 데이터 컨테이너가 필요하다면 namedtuple을 활용할 것
  • 내부 상태를 표현하는 딕셔너리가 복잡해지면 이 데이터를 관리하는 코드를 여러 클래스로 나눠서 재작성 할 것

 

728x90
댓글