본문 바로가기

프로그래머/Python

[Effective Python 복습] Chapter 3. 클래스와 상속

파이썬 코딩의 기술

Chapter3. 클래스와 상속

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

  • 다른 딕셔너리나 긴 튜플을 값으로 담은 딕셔너리를 생성하지 말자
  • 정식 클래스의 유연성이 필요 없다면 가변운 불변 데이터 컨테이너에는 namedtuple을 사용하자
  • 내부 상태를 관리하는 딕셔너리가 복잡해지면 여러 헬퍼 클래스를 사용하는 방식으로 관리 코드를 바꾸자
class WieghtedGradebook(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, [])
        grade_list.append((score, weight))

    def average(self, name):
        by_subject = self._grades[name]
        score_num, score_count = 0, 0
        for subject, scores in by_subject.items():
            subject_avg, total_wight = 0, 0
            for score, weight in scores:
                # ...
        return score_sum / score_count

book.report_grade('Albert', 'Math', 80, 0,10)
  • 값을 튜플로 만든 것뿐이라서 report_grade를 수정한 내역은 간단해 보이지만
  • average_grade 메서드는 루프 안에 루프가 생겨서 이해하기 어려워졌다
  • 클래스를 사용하는 면에서도 위치 인수에 있는 숫자들이 무엇을 의미하는지도 불명확
import collections

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

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:
            total += grade.score * grade.weight
            total_weight += grade.weight
        return total / total_weight

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):
        total, count = 0, 0
        for subject in self._subjects.values():
            total += subject.average_grade()
            count += 1
        return total / count

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')
math = albert.subject('Math')
math.report_grade(80, 0.10)
# ...
print(albert.average_grade())

23. 인터페이스가 간단하면 클래스 대신 함수를 받자

  • 파이썬에서 컴포넌트 사이의 간단한 인터페이스용으로 클래스를 정의하고 인스턴스를 생성하는 대신에 함수만 써도 종종 충분하다
  • 파이썬에서 함수와 메서드에 대한 참조는 일급이다. 즉, 다른 타입처럼 표현식에서 사용할 수 있다
  • call이라는 특별한 메서드는 클래스의 인스턴스를 일반 파이썬 함수처럼 호출할 수 있게 해준다
  • 상태를 보존하는 함수가 필요할 때 상태 보존 클로저를 정의하는 대신 call 메서드를 제공하는 클래스를 정의하는 방안을 고려하자

상태 보존 클로저를 기본값 후크로 사용하는 헬퍼함수

def increment_with_report(current, increments):
    added_count = 0

    def missing():
        nonlocal added_count    # 상태보존클로저
        added_count += 1
        return 0

    result = defaultdict(missing, current)
    for key, amount in increments:
        result[key] += amount

    return result, added_count

result, count = increment_with_report(current, increments)
assert count == 2
  • 클로저 안에 상태를 숨기면 나중에 기능을 추가하기 쉽다

  • 이해하기 어렵다

    보존할 상태를 캡슐화하는 작은 클래스를 정의

  • class CountMissing(object): def __init__(self): self.added = 0 def missing(self): self.added += 1 return 0

counter = CounterMissing()
result = defaultdict(counter.missing, current) # 메서드 참조

for key, amount in increments:
result[key] += amount
assert counter.added == 2

- 헬퍼 클래스로 상태 보존 클로저의 동작을 제공하는 방법이 보다 명확
- 클래스 자체만으로는 용도가 무엇인지 바로 이해하기 어렵다

> __call__ 메서드
```python
class BetterCountMissing(object):
    def __init__(self):
        self.added = 0

    def __call__(self):
        self.added += 1
        return 0

counter = BetterCountMissing()
counter()
assert callable(counter)

counter = BetterCountMissing()
result = defaultdict(counter, current) # __call__이 필요함
for key, amount in increments:
    result[key] += amount
assert counter.added == 2

24. 객체를 범용으로 생성하려면 @classmethod 다형성을 이용하자

  • 파이썬에서는 클래스별로 생성자를 한 개(init 메서드)만 만들 수 있다
  • 클래스에 필요한 다른 생성자를 정의하려면 @classmethod를 사용하자
  • 구체 서브 클래스들을 만들고 연결하는 범용적인 방법을 제공하려면 클래스 메서드 다형성을 이용하자
    class InputData(object):
      def read(self):
          raise NotImplementedError
    

class PathInputData(InputData):
def init(self, path):
super().init()
self.path = path

def read(self):
    return open(self.path).read()

class Worker(object):
def init(self, input_data):
self.input_data = input_data
self.result = None

def map(self):
    raise NotImplementedError

def reduce(self, other):
    raise NotImplementedError

class LineCountWorker(Worker):
def map(self):
data = self.input_data.read()
self.result = data.count('\n')

def reduce(self, other):
    self.result += other.result

def generate_inputs(data_dir):
for name in os.listdir(data_dir):
yield PathInputData(os.path.join(data_dir, name))

def create_workers(input_list):
workers = []
for input_data in input_list:
workers.append(LineCountWorker(input_data))
return workers

def execute(workers):
threads = [Thread(target=w.map) for w in workers]
for thread in threads: thread.start()
for thread in threads: thread.join()

first, rest = workers[0], workers[1:]
for worker in rest:
    first.reduce(worker)
return first.result

def mapreduce(data_dir):
inputs = generate_inputs(data_dir)
workers = create_workers(inputs)
return execute(workers)

- mapreduce 함수가 전혀 범용적이지 않음

> @classmethod 다형성 이용
```python
class GenericInputData(object):
    def read(self):
        raise NotImplementedError

    @classmethod
    def generate_inputs(cls, config):
        raise NotImplementedError

class PathInputData(GenericInputData):
    # ...
    def read(self):
        return open(self.path).read()

    @classmethod
    def generate_inputs(cls, config):
        data_dir = config['data_dir']
        for name in os.listdir(data_dir):
            yield cls(os.path.join(data_dir, name))

class GenericWorker(object):
    # ...
    def map(self):
        raise NotImplementedError

    def reduce(self, other):
        raise NotImplementedError

    @classmethod
    def create_workers(cls, input_class, config):
        workers = []
        for input_data in input_class.generate_inputs(config):
            workers.append(cls(input_data))
        return workers

def mapreduce(worker_class, input_class, config):
    workers = worker_class.create_workers(input_class, config)
    return execute(workers)

with TemporaryDirectory() as tmpir:
    write_test_files(tmpdir)
    config = {'data_dir': tmpdir}
    result = mapreduce(LineCounterWorker, PathInputData, config)

25. super로 부모 클래스를 초기화하자

  • 파이썬의 표준 메서드 해석 순서(MRO)는 슈퍼클래스의 초기화 순서와 다이아몬드 상속 문제를 해결한다
  • 항상 내장 함수 super로 부모 클래스를 초기화하자
    # 파이썬2
    class MyBaseClass(object):
      def __init__(self, value):
          self.value = value
    

class TimesFiveCorrect(MyBaseClass):
def init(self, value):
super(TimesFiveCorrect, self).init(value)
self.value *= 5

class PlusTwoCorrect(MyBaseClass):
def init(self, value):
super(PlusTwoCorrect, self).init(value)
self.value += 2

class GoodWay(TimesFiveCorrect, PlusTwoCorrect):
def init(self, value):
super(GoodWay, self).init(value)

foo = GoodWay(5)

5 * (5 + 2) = 35


- 순서는 뒤에서부터 시작하는 것과 같다(MRO)
- 모든 초기화 메서드는 실제 __init__ 함수가 호출된 순서의 역순으로 실행된다
- 문법이 장황하다
- super를 호출하면서 현재 클래스의 이름을 지정해야 한다

> MRO 순서는 mro 클래스 메서드로 알 수 있다.
```python
from pprint import pprint
pprint(GoodWay.mro())
# 파이썬3
class Explicit(MyBaseClass):
    def __init__(self, value):
        super(__class__, self).__init__(value*2)

class Implicit(MyBaseClass):
    def __init(self, value):
        super().__init__(value*2)

assert Explicit(10).value == Implicit(10).value
  • 파이썬3에서는 super를 인수 없이 호출하면 class와 self를 인수로 넘겨서 호출한 것으로 처리해서 이 문제를 해결

26. 믹스인 유틸리티 클래스에만 다중 상속을 사용하자

  • 믹스인이란 클래스에서 제공해야 하는 추가적인 메서드만 정의하는 작은 클래스를 말한다

  • 믹스인 클래스로 같은 결과를 얻을 수 있다면 다중 상속을 사용하지 말자

  • 인스턴스 수준에서 동작을 교체할 수 있게 만들어서 믹스인 클래스가 요구할 때 클래스별로 원하는 동작을 하게 하자

  • 간단한 동작으로 복잡한 기능을 생성하려면 믹스인을 조합하자

    class ToDictMixin(object):
      def to_dict(self):
          return self._traverse_dict(self.__dict__)
    
      def _traverse_dict(self, instance_dict):
          output = {}
          for key, value in instance_dict.items():
              output[key] = self._traverse(key, value)
          return output
    
      def _traverse(self, key, value):
          if isinstance(value, ToDictMixin):
              return value.to_dict()
          elif isinstance(value, dict):
              return self._traverse_dict(value)
          if isinstance(value, list):
              return [self._traverse(key, i) for i in value]
          if hasattr(value, '__dict__'):
              return self._traverse_dict(value.__dict__)
          else:
              return value
    

class BinaryTree(ToDictMixin):
def init(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right

tree = BinaryTree(10,
left = BinaryTree(7, right=BinaryTree(9)),
right = BinaryTree(13, left=BinaryTree(11)))
print(tree.to_dict())

- 믹스인의 가장 큰 장점은 범용 기능을 교체할 수 있게 만들어서 필요할 때 동작을 오버라이드 할 수 있다는 점

```python
class BinaryTreeWithParent(BinaryTree):
    def __init__(self, value, left=None, right=None, parent=None):
        super().__init__(value, left=left, right=right)
        self.parent = parent

    def _traverse(self, key, value):
        if(isinstance(value, BinaryTreeWithParent) and
            key == 'parent'):
            return value.value # 순환 방지
        else:
            return super()._traverse(key, value)

root BinaryTreeWithParent(10)
root.left = BinaryTreeWithParent(7, parent=root)
root.left.right = BinaryTreeWithParent(9, parent=root.left)
print(root.to_dict())
  • _traverse 메서드를 오버라이드해서 부모를 탐색하지 않고 부모의 숫자 값만 꺼내오게 만든 예제다
class NamedSubTree(ToDictMixin):
    def __init__(self, name, tree_with_parent):
        self.name = name
        self.tree_with_parent = tree_with_parent

my_tree = NamedSubTree('foobar', root.left.right)
print(my_tree.to_dict()) # 무한 루프를 돌지 않음
  • BinaryTreeWithParent 타입의 속성이 있는 클래스라면 무엇이든 자동으로 ToDictMixin으로 동작할 수 있게 되었다
  • 믹스인을 조합할 수도 있다
class JsonMix(object):
    @classmethod
    def from_json(cls, data):
        kwargs = json.loads(data)
        return cls(**kwargs)

    def to_json(self):
        return json.dumps(self.to_dict())

class DatacenterRack(ToDictMixin, JsonMixin):
    def __init__(self, switch=None, machines=None):
        self.switch = Switch(**switch)
        self.machines = [
            Machine(**kwargs) for kwargs in machines
        ]

class Switch(ToDictMixin, JsonMixin):
    # ...

class Machine(ToDictMixin, JsonMixin):
    # ...

deserialized = DatacenterRack.from_json(serialized)
roundtrip = deserialized.to_json()
assert json.loads(serialized) == json.loads(roundtrip)

27. 공개 속성보다는 비공개 속성을 사용하자

  • 파이썬 컴파일러는 비공개 속성을 엄격하게 강요하지 않는다
  • 서브클래스가 내부 API와 속성에 접근하지 못하게 막기보다는 처음부터 내부 API와 속성으로 더 많은 일을 할 수 있게 설계하자
  • 비공개 속성에 대한 접근을 강제로 제어하지 말고 보호 필드를 문서화해서 서브클래스에 필요한 지침을 제공하자
  • 직접 제어할 수 없는 서브클래스와 이름이 충돌하지 않게 할 때만 비공개 속성을 사용하는 방안을 고려하자

    비고개 필드는 속성 이름 앞에 밑줄 두 개를 붙여 지정한다

class MyObject(object):
    def __init__(self):
        self.public_field = 5
        self.__private_field = 10

    def get_private_field(self):
        return self.__private_field

foo = MyObject()
assert foo.public_field == 5
assert foo.get_private_field() == 10

클래스 메서드도 같은 class 블록에 선언되어 있으므로 비공개 속성에 접근할 수 있다

class MyOtherObject(object):
    def __init__(self):
        self.__private_field = 71

    @classmethod
    def get_private_field_of_instance(cls, instance):
        return instance.__private_field

bar = MyOtherObject()
assert MyOtherObject.get_private_field_of_instance(bar) == 71
class MyParentObject(object):
    def __init__(self):
        self.__private_field = 71

class MyChildObject(MyParentObject):
    def get_private_field(self):
        return self.__private_field

bas = MyChildObject()
baz.get_private_field()    # error

assert baz._MyParentObject__private_field == 71
  • 서브 클래스에서는 부모 클래스의 비공개 필드에 접근할 수 없다
  • 비공개 속성의 동작은 간단하게 속성 이름을 변환하는 방식으로 구현된다
class ApiClass(object):
    def __init__(self):    
        self.__value = 5

    def get(self):
        return self.__value

class Child(ApiClass):
    def __init__(self):
        super().__init()
        self._value = 'hello' # OK!

a = Child()
print(a.get(), 'and', a._value, 'are different') # 5 and hello are different
  • 부모 클래스에서 비공개 속성을 사용해서 자식 클래스와 속성 이름이 겹치지 않게 하면 된다

28. 커스텀 컨테이너 타입은 collections.abc의 클래스를 상속 받게 만들자

  • 쓰임새간 간단할 때는 list나 dict 같은 파이썬의 컨테이너 타입에서 직접 상속받게 하자
  • 커스텀 컨테이너 타입을 올바르게 구현하는 데 필요한 많은 메서드에 주의해야 한다
  • 커스텀 컨테이너 타입이 collections.abc에 정의된 인터페이스에서 상속받게 만들어서 클래스가 필요한 이넡페이스, 동작과 일치하게 하자
class BinaryNode(object):
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right
  • 이 클래스가 시퀀스 타입처럼 동작하게 하려면?
    • 파이썬은 특별한 이름을 붙이 인스턴스 메서드로 컨테이너 동작을 구현한다

예시

bar = [1,2,3]
assert bar[0] == bar.__getitem__(0)
  • BinaryNode 클래스가 시퀀스처럼 동작하게 하려면 객체의 트리를 깊이 우선으로 탐색하는 getitem을 구현하면 된다
class IndexableNode(BinaryNode):
    def _search(self, count, index):
        # ...
        # (found, count) 반환

    def __getitem__(self, index):
        found, _ = self._search(0, index)
        if not found:
            raise IndexError('Index out of range')
        return found.value

tree = IndexableNode(
    10,
    left = IndexableNode(,
        5,
        left = IndexableNode(2),
        right = IndexableNode(
            6, right = IndexableNode(7))),
    right = IndexableNode(
        15, left = IndexableNode(11)))
  • 트리 탐색은 물론이고 list처럼 접근할 수도 있다
  • 문제는 getitem을 구현하는 것만으로는 기대하는 시퀀스 시맨틱을 모두 제공하지 못한다는 점이다 (ex. len(tree))
class SequenceNode(IndexableNode):
    def __len__(self):
        _, count = self._search(0, None)
        return count

tree = SequenceNode(
    # ...
)

print('Tree has %d nodes' % len(tree))
  • count, index 메서드가 빠졌다...
from collections.abc import Sequence

class BetterNode(SequenceNode, Sequence):
    pass

tree = BetterNode(
    # ...
)

print('Index of 7 is', tree.index(7))
print('Count of 10 is', tree.cound(10))