파이썬 코딩의 기술 - 41
기능을 합성할 때는 믹스인 클래스를 사용하라
파이썬은 다중 상속을 처리할 수 있게 지원하는 객체지향 언어이다. (하지만 다중 상속은 피하는 편이 좋다.)
다중 상속이 제공하는 편의와 캡슐화가 필요하지만 다중 상속으로 인해 발생할 수 있는 문제들을 피하고 싶다면, 믹스인(mix-in) 을 사용할 지 고려야 보아야 한다.
믹스인은 자식 클래스가 사용할 메서드 몇 개만 정의하는 클래스이다. 믹스인 클래스에는 자체 애트리뷰트 정의가 없으므로 믹스인 클래스의 __init__
메서드를 호출할 필요도 없다.
믹스인(mix-in)
믹스인(mix-in)은 다른 클래스에서 사용할 수 있도록 공통적인 메서드를 모아 놓은 클래스를 말한다. 파이썬에서 믹스인은 자체 인스턴스 속성을 가지고 있지 않으며__init__
메서드를 구현하지 않는다. 예를 들어 인사하는 메서드greeting
를 사람 종류의 클래스에서 공통적으로 사용하는 메서드로HelloMixIn
을 구현했을 때,Student
는HelloMixIn
과Person
을 상속받고,Teacher
도HelloMixIn
과Person
을 상속받으면Student
와Teacher
는 모두 공통 메서드인greeting
을 사용할 수 있다.class HelloMixIn: def greeting(self): # 인사하는 메서드는 공통적인 메서드 print('안녕하세요.') class Person(): def __init__(self, name): self.name = name class Student(HelloMixIn, Person): # HelloMixIn과 Person을 상속받아 학생 클래스를 만듦 def study(self): print('공부하기') class Teacher(HelloMixIn, Person): # HelloMixIn과 Person을 상속받아 선생님 클래스를 만듦 def teach(self): print('가르치기')
예를 들어 메모리 내에 들어 있는 파이썬 객체를 직렬화에 사용할 수 있도록 딕셔너리로 바꾸고 싶다고 하자.
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)
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}
}
믹스인의 가장 큰 장점은 제네릭 기능을 쉽게 연결할 수 있고 필요할 때 기존 기능을 다른 기능으로 오버라이드(override)해 변경할 수 있다는 것이다. 예를 들어 다음 코드는 BinaryTree
에 대한 참조를 저장하는 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
해결 방법은 BinaryTreeWithParent._traverse
메서드를 오버라이드해 문제가 되는 값만 처리하게 만들어서 믹스인이 무한 루프를 돌지 못하게 하는 것이다.
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())
>>>
{'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으로 직렬화하거나 역직렬화하는 것은 간단하다.
deserialized = DatacenterRack.from_json(serialized)
roundtrip = deserialized.to_json()
assert json.loads(serialized) == json.loads(roundtrip)
이렇게 믹스인을 사용할 때 JsonMixin
을 적용하려고 하는 클래스 상속 계층의 상위 클래스에 이미 JsonMixin
을 적용한 클래스가 있어도 아무런 문제가 없다. 이런 경우에도 super
가 동작하는 방식으로 인해 믹스인을 적용한 클래스가 제대로 작동한다.
기억해야 할 내용
- 믹스인을 사용해 구현할 수 있는 기능을 인스턴스 애트리뷰트와
__init__
을 사용하는 다중 상속을 통해 구현하지 말라.- 믹스인 클래스가 클래스별로 특화된 기능을 필요로 한다면 인스턴스 수준에서 끼워 넣을 수 있는 기능(정해진 메서드를 통해 해당 기능을 인스턴스가 제공하게 만듦, 오버라이딩)을 활용하라.
- 믹스인에는 필요에 따라 인스턴스 메서드는 물론 클래스 메서드도 포함할 수 있다.
- 믹스인을 합성하면 단순한 동작으로부터 더 복잡한 기능을 만들어 낼 수 있다.
Author And Source
이 문제에 관하여(파이썬 코딩의 기술 - 41), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@primadonna/파이썬-코딩의-기술-41저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)