[Python] Skill of coding - 딕셔너리와 튜플보다는 헬퍼 클래스로 관리하기

Hyeseong·2020년 12월 10일
0

python skill of coding

목록 보기
15/18

딕셔너리와 튜플보다는 헬퍼 클래스로 관리하기

파이썬에 내장되어 있는 딕셔너리 타입은 객체의 수명이 지속되는 동안 동적인 내부 상태를 관리하는 용도로 아주 좋다. 여기서 '동적'이란 예상하지 못한 식별자들을 관리해야 하는 상황을 뜻한다.

예를 들어 이름을 모르는 학생 집단의 성적을 기록하고 싶다고 해보자. 학생별로 미리 정의된 속성을 사용 하지 않고 딕셔너리 이름을 저장하는 클래스를 정의 할 수 있다.

class SimpleGradebook(object):
    def __init__(self):
        self._grades = {}

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

    def report_grade(self, name, score):
        self._grades[name].append(score)
    

클래스를 사용하는 방법은 간단하다.

book = SimpleGradebook()
book.add_student('Issac Newton')
book.report_grade('Issac Newton', 90)
print(book.average_grade('Issac Newton'))
print(book.__dict__)
90.0
{'_grades': {'Issac Newton': [90]}}

딕셔너리는 정말 사용하기 쉬워서 과도하게 쓰다가 코드를 취약하게 작성할 위험이 있다. 예를들어 SimpleGradebook 클래스를 확장해서 모든 성적을 한 곳에 저장하지 않고 과목별로 저장한다고 하자. 이런 경우 _grades딕셔너리를 변경해서 학생 이름(키)을 또 다른 딕셔너리(값)에 매핑하면 된다. 가장 안쪽에 있는 딕셔너리는 과목(키)을 성적(값)에 매핑한다.

class BySubjectGradebook(object):
    def __init__(self):
        self._grades = {}

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

    def report_grade(self, name, subject, grade):
        by_subject = self._grades[name]
        grade_list = by_subject.setdefault(subject, [])

위의 코드는 충분히 직관적이다. report_grade와 average_grade 메서드는 여러 단계의 딕셔너리를 처리하느라 약간 복잡해지지만 아직은 다룰만하다.

클래스를 사용하는 방법은 여전히 간단하다.

book = BySubjectGradebook()
book.add_student('Albert Einstein')
book.report_grade('Albert Einstein', 'Math', 75)
book.report_grade('Albert Einstein', 'Math', 65)
book.report_grade('Albert Einstein', 'Gym', 90)
book.report_grade('Albert Einstein', 'Gym', 95)
print(book.average_grade('Albert Einstein'))
print(book.__dict__)
81.25
{'_grades': {'Albert Einstein': {'Math': [75, 65], 'Gym': [90, 95]}}}

이제 요구 사항이 다시 바뀐다고 해보자. 수업의 최종 성적에서 각 점수가 차지하는 비중을 매겨서 중간고사와 기말고사를 쪽지시험보다 중요하게 만들려고 한다. 이 기능을 구현하는 방법 중 하나는 가장 안쪽 딕셔너리를 변경해서 과목(키)을 성적(값)에 매핑하지 않고, 성적과 비중을 담은 튜플(score, weight)에 매핑하는 것이다.


class WeightedGradebook(object):
    def __init__(self):
        self._grades = {}

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

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

값을 튜플로 만든 것뿐이라서 report_grade를 수정한 내역은 간단해 보이지만, average_grade 메서드는 루프 안에 루프가 생겨서 이해하기 어려워졌다.

클래스를 사용하는 방법도 더 어려워졌다. 위치 인수에 있는 숫자들이 무엇을 의미하는지도 명확하지 않다.


book = WeightedGradebook()
book.add_student('Albert Einstein')
book.report_grade('Albert Einstein', 'Math', 80, 0.10)
book.report_grade('Albert Einstein', 'Math', 80, 0.10)
book.report_grade('Albert Einstein', 'Math', 70, 0.80)
book.report_grade('Albert Einstein', 'Gym', 100, 0.40)
book.report_grade('Albert Einstein', 'Gym', 85, 0.60)
print(book.average_grade('Albert Einstein'))
81.5

이렇게 복잡해지면 딕셔너리와 튜플 대신 클래스의 계층 구조를 사용할 떄가 된 것이다.

처음엔 성적에 비중을 적용하게 될지 몰랐으니 복잡하게 헬퍼 클래스를 추가할 필요까지는 없을 것 같았다. 파이썬의 내장 딕셔너리와 튜플 타입을 쓰면 내부 관리용으로 층층이 타입을 추가하는 게 쉽다. 하지만 계층이 한 단계가 넘는 중첩은 피해야 한다.(즉, 딕셔너리를 담은 딕셔너리는 쓰지 말아야한다.) 여러 계층으로 중첩하면 다른 프로그래머들이 코드를 이해하기 어려워지고 유지보수의 악몽에 빠지게 된다.

관리하기 복잡하다고 느끼는 즉시 클래스로 옮겨가야한다. 그러면 데이터를 더 잘 캡슐화한 잘 정의된 인터페이스를 제공할 수 있다. 또한 인터페이스와 실제 구현 사이에 추상화 계층을 만들 수 있다.

클래스 리팩토링

의존 관계에서 가장 아래에 있는 성적부터 클래스를 옮겨보자. 이렇게 간단한 정보를 담기에 클래스는 너무 무거워 보인다. 성적은 변하지 않으니 튜플을 사용하는게 더 적절해 보인다. 다음 코드에서는 리스트 안에 성적을 기록 하려고(score, weight) 튜플을 사용한다.

grades = []
grades.append((95, 0.45))
grades.append((85, 0.55))
total = sum(score * weight for score, weight in grades)
print(total)
total_weight = sum(weight for _, weight in grades)
print(total_weight)
average_grade = total / total_weight
print(average_grade)
89.5
1.0
89.5

문제는 일반 튜플은 위치에 의존한다는 점이다. 성적에 선생님의 의견 같은 더 많은 정보를 연관지으려면 이제 튜플을 사용하는 곳을 모두 찾아서 아이템 두 개가 아니라 세 개를 쓰도록 수정해야 한다. 다음 코드에서는 튜플에 있는 세 번째 값을 _로 받아서 그냥 무시하도록 했다

grades = []
grades.append((95, 0.45, 'Great job'))
grades.append((85, 0.55, 'Better next time'))
total = sum(score * weight for score, weight, _ in grades)
print(total)
total_weight = sum(weight for _, weight, _ in grades)
print(total_weight)
average_grade = total / total_weight
print(average_grade)
89.5
1.0
89.5

튜플을 점점 더 길게 확장하는 패턴은 딕셔너리의 계층을 깊게 두는 방식과 비슷하다. 튜플의 아이템이 두 개를 넘어가면 다른 방법을 고려해야 한다.

collections 모듈의 nametuple 타입이 정확히 이런 요구에 부합한다. nametuple을 이용하면 작은 불변 데이터 클래스(immutable data class)를 쉽게 정의 할 수 있다.

import collections
Grade = collections.namedtuple('Grade', ('score', 'weight'))

불변 데이터 클래스는 위치 인수나 키워드 인수로 생성할 수 있다. 필드는 이름이 붙은 속성으로 접근할 수 있다. 이름이 붙은 속성이 있으면 나중에 요구 사항이 또 변해서 단순 데이터 컨테이너에 동작을 추가해야 할 때 nametuple에서 직접 작성한 클래스로 쉽게 바꿀 수 있다.

namedtuple의 제약

namedtuple이 여러 상황에서 유용하긴 하지만 장점보다 단점을 만들어낼 수 있는 상황도 이해해봐요.

namedtuple로 만들 클래스에 기본 인수 값을 설정할 수 없다. 그래서 데이터에 선택적인 속성이 많으면 다루기 힘들어져요. 속성을 사용할 때는 클래스를 직접 정의하는게 나을 수 있다.

namedtuple 인스턴스의 속성 값을 여전히 숫자로 된 인덱스와 순회 방법으로 접근할 수 있다. 특히 외부 API로 노출한 경우에는 의도와 다르게 사용되어 나중에 실제 클래스로 바꾸기 더 어려울 수도 있다. namedtuple 인스턴스를 사용하는 방식을 모두 제어할 수 없다면 클래스를 직접 정의하는게 낫다.

다음으로 성적들을 담은 단일 과목을 표현하는 클래스를작성해보자.

class Subject(object):
    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:

이제 한 학생이 공부한 과목들을 표현하는 클래스를 작성해보자

class Student(object):
    def __init__(self):
        self._subjects = {}

    def subject(self, name):
        if name not in self._subjects:
            self._subjects[name] = Subject()
        return self._subjects[name]

    def average_grade(self):

마지막으로 학생의 이름을 키로 사용해 동적으로 모든 학생을 담을 컨테이너를 작성한다.


class Gradebook(object):
    def __init__(self):
        self._students = {}

    def student(self, name):
        if name not in self._students:
            self._students[name] = Student()
        return self._students[name]
book = Gradebook()
albert = book.student('Albert Einstein')
math = albert.subject('Math')
math.report_grade(80, 0.10)
math.report_grade(80, 0.10)
math.report_grade(70, 0.80)
gym = albert.subject('Gym')
gym.report_grade(100, 0.40)
gym.report_grade(85, 0.60)
print(albert.average_grade())
81.5

위 클래스의 코드 줄 수는 이전에 구현한 코드의 두 배에 가깝다. 하지만 이 코드가 훨씬 이해하기 숩다. 이클래스들을 사용하는 예제도 더 명확하고 확장하기 쉽다.

필요하면 이전 형태의 API 스타일로 작성한 코드를 새로 만든 객체 계층 스타일로 바꿔주는 하위 호환용 메서드를 작성해도 된다.

핵심정리

  • 다른 딕셔너리나 긴 튜플을 값으로 딕셔너리를 생성하지 말자
  • 정식 클래스의 유연성이 필요 없다면 가벼운 불변 데이터 컨테이너에는 namedtuple을 사용하자.
  • 내부 상태를 관리하는 딕셔너리가 복잡해지면 여러 헬퍼 클래스를 사용하는 방식으로 관리 코드를 바꾸자
profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글