betterway41합성할때 믹스인 클래스이용

김승환·2021년 7월 11일

코딩의 기술

목록 보기
31/36

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

  • 파이썬은 다중 상속을 처리할 수 있게 지원하는 객체지향 언어
  • 하지만 다중 상속은 피하는편이 좋다.

다중 상속이 제공하는 편의와 캡슐화가 필요하지만 다중 상속으로 인해 발생할 수 있는 골치아픈 경우를 피하고싶으면 믹스인을 써라

  • 믹스인(mix-in)은 자식 클래스가 사용할 메서드 몇 개만 정의하는 클래스

  • 믹스인 클래스에는 자체 애트리부트 정의가 없으므로 믹스인 클래스의 init 메서드를 호출할 필요도 없다.

  • 파이썬에서는 타입과 상관없이 객체의 현재 상태를 쉽게 들여다볼 수 있으므로 익스인 작성이 쉽다.

  • 동적인 상태 접근이 가능하다는 말은 제너릭인 기능을 믹스인 안에 한번만 작성해두면 다른 여러 클래스에 적용할 수 있다는 뜻

  • 믹스인을 합성하거나 계층화해서 반복적인 코드를 최소화하고 재사용성을 최대화 할 수 있다.

  • 예를 들어 메모리 내에 들어 있는 파이썬 객체를 직렬화에 사용할 수 있도록 딕셔너- 리로 바꾸고싶다고 하자.

  • 이런기능을 클래스로 활용했을때 아래 예시

#클래스를 사용한 경우, 
# 이런 기능을 제공하는 공개 메서드를 사용해 정의한 믹스인 예쩨
# 믹스인을 상송하는 모든 클래스에서 이 함수의 기능 사용가능
class ToDictMixin:
    def to_dict(self):
        return self._traverse_dict(self.__dict__)
    #_Traverse_dict 메서드를 hasattr을 통한 동적인 애트리뷰트 접근과 isinstance를 사용한 타입검사
    #__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}}

믹스인의 장점

  • 제너릭 기능을 쉽게 연결할 수 있고 필요할 때 기존 기능을 다른 기능으로 오버라이드해 변경가능
  • 예를 들어 다음 코드는 BinatyTree에 대한 참조를 저장하는 BinaryTree의 하위 클래스를 정의한다.
  • 이런 순환 참조가 있으면 원래의 ToDictMixin.to_dict구현은 무한 루프를 돈다.
class BinaryTreeWithParent(BinaryTree):
    def __init__(self, value, left=None,
                 right=None, parent=None):
        super().__init__(value, left=left, right=right)
        self.parent = parent
    #무한 루프를 도는 것을 막기위해 super를 사용하여 디폴트 믹스인 구현을 호출
    def _traverse(self, key, value):
        if (isinstance(value, BinaryTreeWithParent) and
                key == 'parent'):
            return value.value  # Prevent cycles
        else:
            return super()._traverse(key, value)

위 같이 하면 변환 시 순환 참조를 따라가지 않으므로 BinaryTreeWithParent.to_dict가 잘 작동한다.

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}

BinaryTreeWithParent._traverse를 오버라이드함에 따라 BinaryTreeWithParent를 애트리뷰트로 저장하는 모든 클래스도 자동으로 ToDictMixin을 문제없이 사용할 수 있게 된다.

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}}

믹스인 합성

  • 예를 들어 임의의 클래스를 json으로 직렬화 하는 제너릭 믹스인을 만들고 싶다고 하자.
  • 모든 클래스가 to_dict 메서드를 제공한다고 가정하면
  • 다음과 같이 제너릭 믹스인을 만들 수 있다
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())
  • 여기서 JsonMixin클래스 안에 인스턴스 메서드와 클래스 메서드가 함께 정의됐다는 점에 유의

  • 믹스인을 사용하면 인스턴스의 동작이나 클래스의 동작 중 어느 것이든 하위 클래스에 추가할 수 있다.

  • 예제에서 JsonMixin하위 클래스의 요구 사항은 to_dict 메서드를 제공해야 한다는 점과 init 메서드가 키워드 인자를 받아야 한다는 점 뿐이다.

  • 이런 믹스인이 있으면 Json과 직렬화를 하거나 역직렬화를 할 유틸리티 클래스의 클래스 계층구조를 쉽게, 번잡스러운 준비 코드 없이 만들 수 있다.

  • 예를 들어 데이터 센터의 각 요소 간 연결을 표현하는 클래스 계층이 있다고 하자(topology)

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
  • 이런 클래스들을 json으로 직렬화하거나 json으로부터 역질렬화 하는 것 은 간단하다.
  • 다음은 데이터를 json으로 직렬화한 다음에 다시 역직렬화하는 양방향 변환이 가능한지 검사하는 코드다
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)
  • 이렇게 믹스인을 사용할 때 jsonMixin을 적용하려고 하는 클래스 상속 계층의 상위 클래스에 이미 JsonMixin을 적용한 클래스가 있어도 아무런 문제가 없다.
  • 이런 경우에도 super가 동작하는 방식으로 인해 믹스인을 적용한 클래스가 제대로 작동한다.
profile
인공지능 파이팅!

0개의 댓글