Chapter 5. 클래스와 인터페이스를 읽으면서 정리합니다.

새로 알게 되었거나 중요하다고 생각되는 부분 위주로 정리하고자 합니다.

Effective Python 2nd

Chapter 5. 클래스와 인터페이스

37. 내장 타입을 여러 단계로 내포시키기보다는 클래스를 합성하라

  • 딕셔너리, 긴 튜플, 다른 내장 타입이 복잡하게 내포된 데이터를 값으로 사용하는 딕셔너리를 만들지 말라.

  • 완전한 클래스가 제공하는 유연성이 필요하지 않고 가벼운 불변 데이터 컨테이너가 필요하다면 nametuple을 사용하라.

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

38. 간단한 인터페이스의 경우 클래스 대신 함수를 받아라

  • 파이썬의 여러 컴포넌트 사이에 간단한 인터페이스가 필요할 때는 클래스를 정의하고 인스턴스화하는 대신 간단히 함수를 사용할 수 있다.

  • __call__ 특별 메서드를 사용하면 클래스의 인스턴스인 객체를 일반 파이썬 함수처럼 호출할 수 있다.

  • 상태를 유지하기 위한 함수가 필요한 경우에는 상태가 있는 클로저를 정의하는 대신 __call__ 메서드가 있는 클래스를 정의할지 고려하라.

    class BetterCountMissing:
        def __init__(self):
            self.added = 0
      
        def __call__(self):
            self.added += 1
            return 0
      
    counter = BetterCountMissing()
    assert counter() == 0
    assert callable(counter)
      
    counter = BetterCountMissing()
    result = defaultdict(counter, current) # __call__에 의존함
    for key, amount in increments:
        result[key] += amount
    assert counter.added == 2
    

39. 객체를 제너릭하게 구성하려면 @classmethod를 통한 다형성을 활용하라

  • 파이썬의 클래스에는 생성자 __init__ 메서드뿐이다.

  • @classmethod를 사용하면 클래스에 다른 생성자를 정의할 수 있다.

  • 클래스 메서드 다형성을 활용하면 여러 구체적인 하위 클래스의 객체를 만들고 연결하는 제너릭한 방법을 제공할 수 있다.

    ### GenericInputData와 PathInputData를 사용한 방법
    class GenericInputData:
        def read(self):
            raise NotImplementedError
      
        @classmethod
        def generate_inputs(cls, config):
            raise NotImplementedError
      
    class PathInputData(GenericInputData):
        def __init__(self, path):
            super().__init__()
            self.path = path
      
        def read(self):
            with open(self.path) as f:
                return f.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:
        def __init__(self, input_data):
            self.input_data = input_data
            self.result = None
      
        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
      
    class LineCountWorker(GenericWorker):
        def map(self):
            data = self.input_data.read()
            self.result = data.count('\n')
      
        def reduce(self, other):
            self.result += other.result
      
    def mapreduce(worker_class, input_class, config):
        workers = worker_class.create_workers(input_class, config)
        return execute(workers)
        
    config = {'data_dir': tmpdir}
    result = mapreduce(LineCountWorker, PathInputData, config)
    print(f'총 {result} 줄이 있습니다.')
    

40. super로 부모 클래스를 초기화하라

  • 표준 메서드 결정 순서(MRO)를 활용해 상위 클래스 초기화 순서와 다이아몬드 상속 문제를 해결한다.

    • 다이아몬드 상속 문제란?

      어떤 클래스가 두 가지 서로 다른 클래스를 상속하는데, 두 상위 클래스의 상위 계층을 거슬러 올라가면 같은 조상 클래스가 존재하는 경우 조상 클래스의 __init__ 메서드가 여러 번 호출 될 수 있다.

  • 부모 클래스를 초기화 할때는 super 내장 함수를 아무 인자 없이 호출하라.

    super를 아무 인자 없이 호출하면 파이썬 컴파일러가 자동으로 올바른 파라미터를 넣어준다.

    class ExplicitTrisect(MyBaseClass):
        def __init__(self, value):
            super(ExplicitTrisect, self).__init__(value)
            self.value /= 3
      
    class AutomaticTrisect(MyBaseClass):
        def __init__(self, value):
            super(__class__, self).__init__(value)
            self.value /= 3
      
    class ImplicitTrisect(MyBaseClass):
        def __init__(self, value):
            super().__init__(value)
            self.value /= 3
      
    assert ExplicitTrisect(9).value == 3
    assert AutomaticTrisect(9).value == 3
    assert ImplicitTrisect(9).value == 3
    

41. 기능을 합성할 때는 믹스인 클래스를 사용하라

  • 믹스인을 사용해 구현할 수 있는 기능을 인스턴스 애트리뷰트와 __init__ 을 사용하는 다중 상속을 통해 구현하지 말라.

  • 믹스인에는 필요에 따라 인스턴스 메서드는 물론 클래스 메서드도 포함될 수 있다.

  • 믹스인을 합성하면 단순한 동작으로부터 더 복잡한 기능을 만들어낼 수 있다.

    class ToDictMixin:
        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)
            elif isinstance(value, list):
                return [self._traverse(key, i) for i in value]
            elif 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())
      
    #{'value': 10, 'left': {'value': 7, 'left': None, 'right': {'value': 9, 'left': None, 'right': None}}, 'right': {'value': 13, 'left': {'value': 11, 'left': None, 'right': None}, 'right': None}}
    
    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  # Prevent cycles
            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())
      
    #{'value': 10, 'left': {'value': 7, 'left': None, 'right': {'value': 9, 'left': None, 'right': None, 'parent': 7}, 'parent': 10}, 'right': None, 'parent': None}
      
    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()) # 무한 루프없음
    # {'name': 'foobar', 'tree_with_parent': {'value': 9, 'left': None, 'right': None, 'parent': 7}}
    
    import json
      
    class JsonMixin:
        @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):
        def __init__(self, ports=None, speed=None):
            self.ports = ports
            self.speed = speed
      
    class Machine(ToDictMixin, JsonMixin):
        def __init__(self, cores=None, ram=None, disk=None):
            self.cores = cores
            self.ram = ram
            self.disk = disk
      
    serialized = """{
        "switch": {"ports": 5, "speed": 1e9},
        "machines": [
            {"cores": 8, "ram": 32e9, "disk": 5e12},
            {"cores": 4, "ram": 16e9, "disk": 1e12},
            {"cores": 2, "ram": 4e9, "disk": 500e9}
        ]
    }"""
      
    deserialized = DatacenterRack.from_json(serialized)
    roundtrip = deserialized.to_json()
    assert json.loads(serialized) == json.loads(roundtrip)
    

42. 비공개 애트리뷰트보다는 공개 애트리뷰트를 사용하라

  • 파이썬 컴파일러는 비공개 애트리뷰트를 자식 클래스나 클래스 외부에서 사용하지 못하도록 엄격히 금지하지 않는다.

  • 비공개 애트리뷰트로 접근을 막으려고 시도하기보다 보호된 필드를 사용하면서 문서에 적절한 가이드를 남겨라.

  • 코드 작성을 제어할 수 없는 하위 클래스에서 이름 충돌이 일어나는 경우를 막고 싶을 때만 비공개 애트리뷰트를 사용하라.

    class ApiClass:
        def __init__(self):
            self._value = 5
      
        def get(self):
            return self._value
      
    class Child(ApiClass):
        def __init__(self):
            super().__init__()
            self._value = 'hello'  # 충돌
    a = Child()
    print(f'{a.get()}{a._value} 는 달라야 합니다.')
    # hello 와 hello 는 달라야 합니다.
      
    class ApiClass:
        def __init__(self):
            self.__value = 5    # 밑줄 2개!
      
        def get(self):
            return self.__value # 밑줄 2개!
      
    class Child(ApiClass):
        def __init__(self):
            super().__init__()
            self._value = 'hello' # OK!
      
    a = Child()
    print(f'{a.get()}{a._value} 는 다릅니다.')
    # 5 와 hello 는 다릅니다.
    

43. 커스텀 컨테이너 타입은 collections.abc를 상속하라

  • 간편하게 사용할 경우에는 파이썬 컨테이너 타입(리스트, 딕셔너리)을 직접 상속하라

    class FrequencyList(list):
        def __init__(self, members):
            super().__init__(members)
      
        def frequency(self):
            counts = {}
            for item in self:
                counts[item] = counts.get(item, 0) + 1
            return counts
      
    foo = FrequencyList(['a', 'b', 'a', 'c', 'b', 'a', 'd'])
    print('길이: ', len(foo))
      
    foo.pop()
    print('pop한 다음:', repr(foo))
    print('빈도:', foo.frequency())
      
    #길이:  7
    #pop한 다음: ['a', 'b', 'a', 'c', 'b', 'a']
    #빈도: {'a': 3, 'b': 2, 'c': 1}
    
  • 커스텀 턴테이너 타입이 collection.abc에 정의된 인터페이스를 상속하면 커스텀 컨테이너 타입이 정상적으로 작동하기 위해 피요한 인터페이스와 기능을 제대로 구현하도록 보장할 수 있다.

    from collections.abc import Sequence
      
    class SequenceNode(IndexableNode):
        def __len__(self):
            for count, _ in enumerate(self._traverse(), 1):
                pass
            return count
      
    class BadType(Sequence):
        pass
      
    # 오류가 나는 부분. 오류를 보고 싶으면 커멘트를 해제할것
    #foo = BadType()
      
    class BetterNode(SequenceNode, Sequence):
        pass
      
    tree = BetterNode(
        10,
        left=BetterNode(
            5,
            left=BetterNode(2),
            right=BetterNode(
                6,
                right=BetterNode(7))),
        right=BetterNode(
            15,
            left=BetterNode(11))
    )
      
    print('7의 인덱스:', tree.index(7))
    print('10의 개수:', tree.count(10))
    # 7의 인덱스: 3
    # 10의 개수: 1