38. 간단한 인터페이스의 경우 클래스 대신 함수를 받아라
요약
- 간단한 인터페이스가 필요할때는 클래스를 만들고 인스턴스로 생성해서 쓰는 대신 간단하게 함수를 활용할 수 있다.
상세
defaultdict는 없는 키로 접근하는 상황에서 호출할 함수를 넘겨 받습니다.
def log_missing():
print('Key added')
return 0
# 특정상황에서 넘겨 준 log_missing함수를 실행
# 인자로 받는 함수를 훅이라고 함
current = {'green': 12, 'blue': 3}
increments = [
('red', 5),
('blue', 17),
('orange', 9),
]
for key, amount in increments:
result[key] += amount
print('After: ', dict(result))
# result
Before: {'green': 12, 'blue': 3}
Key added
Key added
After: {'green': 12, 'blue': 20, 'red': 5, 'orange': 9}
위와 같이 구현하는 경우 정해진 동작과 사이드 이펙트를 분리할 수 있기 때문에 API를 더 쉽게 만들 수 있습니다.
작은 클래스를 만들어 해결하는 방법도 있지만, 클래스만 보면 CountMissing 클래스의 목적이 무엇인지 확실하게 알기 어렵다. 누가 CountMissing의 인스턴스를 만들지, 누가 missing 메소드를 호출할지, 어떤 메소드가 더 추가될지 등등 DeafultDict와 함께 사용되는 예제를 보기 전에는 클래스를 정확하게 파악하기 어려울 수 있다.
class CountMissing:
def __init__(self):
self.added = 0
def missing(self):
self.added += 1
return 0
counter = CountMissing()
for key. amount in increments:
result[key] += amount
assert count.added == 2
이런 경우에는 __call__메소드를 정의하여 사용할 수 있습니다. 객체를 함수처럼 호출할 수 있게 만들어줍니다. __call__이 정의된 클래스의 인스턴스에 대해 callable 내장 함수를 호출하면 다른 메소드와 같이 True가 반환됩니다. 이런 식으로 정의되어 호출할 수 있는 모든 가능 객체를 호출 가능(callable) 객체라고 합니다.
아래는 존재하지 않는 키에 접근한 횟수를 세고 싶은 상황 가정했을때의 예시 코드입니다. 호출 가능(callable) 객체를 사용한 예제입니다.
class BetterCountMissing:
def __init__(self):
self.added = 0
def __call__(self):# 호출 가능 객체
self.added += 1
return 0
current = {'green': 12, 'blue': 3}
increments = [
('red', 5),
('blue', 17),
('orange', 9),
]
# Example 9
counter = BetterCountMissing()
result = defaultdict(counter, current) # __call__에 의존
for key, amount in increments:
result[key] += amount
print(counter.added)# 2
print(dict(result)) # {'green': 12, 'blue': 20, 'red': 5, 'orange': 9}
__call__을 활용한 방식은 함수가 인자로 쓰일 수 있는 부분에 이 클래스의 인스턴를 사용할 수 있다는 사실을 보여줄 수 있습니다.
결론
- 파이썬의 여러 컴포넌트 사이에 간단한 인터페이스가 필요할 때는 클래스를 정의하고 인스턴스화하는 대신 간단히 함수를 사용할 수 있다.
- 파이썬 함수나 메서드는 일급 시민이다. 따라서 (다른 타입의 값과 마찬가지로) 함수나 함수 참조를 식에 사용할 수 있다.
- 특별 메서드를 사용하면 클래스의 인스턴스 객체를 일반 파이썬 함수처럼 호출할 수 있다.
- 상태를 유지하기 위한 함수가 필요한 경우에는 상태가 있는 클로저를 정의하는 대신 __call__ 메서드가 있는 클래스를 정의할지 고려해보라.
39. 객체를 제너릭하게 구성하려면 @classmethod를 통한 다형성을 활용하라
요약
- @classmethod 를 사용해서 하위 클래스를 같은 방식으로 사용할 수 있습니다.
상세
class GenericInputData:
def read(self):
raise NotImplementedError
@classmethod # 클래스메서드로 표시
def generate_inputs(cls, config):
raise NotImplementedError
class PathInputData(GenericInputData):
def __init__(self, i):
super().__init__()
self.path = f'temp_dir/{i}'
def read(self):
return self.path
@classmethod
def generate_inputs(cls, start):# 클래스를 인자로 받음
for i in range(start, start + 5):
yield cls(i) # 인자로 받은 클래스로 인스턴스 생성
# 부모 클래스(GenericInput)로 작성
def temp_generate_inputs(input_class, start):
it = input_class.generate_inputs(start)
return it
# 최초 사용 부분에서 자식 클래스 지정(PathInputData)
data = temp_generate_inputs(PathInputData, 3)
for x in data:# x는 PathInputData 객체
print(x.read())
#print
temp_dir/3
temp_dir/4
temp_dir/5
temp_dir/6
temp_dir/7
정리
- 파이썬의 클래스에는 생성자가 init 메서드 뿐이다.
- @classmethod를 사용하면 클래스에 다른 생성자를 정의할 수 있다.
- 클래스 메서드 다형성을 활용하면 여러 구체적인 하위 클래스의 객체를 만들고 연결하는 제너릭한 방법을 제공할 수 있다.
41. 기능을 합성할때는 믹스인 클래스를 사용하라
파이썬은 다중 상속을 지원하지만 피하는 편이 좋습니다. 다이아몬드 문제, 코드의 복잡성이 증가하는게 그 이유가 됩니다.
다중 상속이 제공하는 편의와 캡슐화가 필요하지만 다중 상속으로 인해 발생할 수 있는 골치아픈 경우는 피하고 싶다면 mix-in을 사용해보는걸 고려할 수 있습니다.
- 믹스인 클래스 : 자식 클래스가 사용할 메소드 몇개만 정의하는 클래스입니다. 믹스인 클래스에는 따로 attribute정의가 없고 믹스인 클래스의 __init__메소드를 호출할 필요 없이 기능만 정의 할 수 있습니다.
공통으로 사용될 기능을 믹스인 안에 한번만 작성해두면 다른 여러 클래스에 적용할 수 있다는 뜻을 가집니다. 믹스인을 합성하거나 계층화해서 반복적인 코드를 최소화하고 재사용성을 극대화 할 수 있습니다.
아래 코드는 공개메소드를 사용해 정의한 믹스인의 예제로 이 믹스인을 상속하는 모든 클래스에서 이 함수의 기능을 사용할 수 있습니다.
class ToDictMixin:
def to_dict(self):# 믹스인을 상속하는 모든 클래스에서 이 함수의 기능을 사용할 수 있음
return self._traverse_dict(self.__dict__)# __dict__ : 인스턴스가 갖는 애트리뷰트들의 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
# Example 3
class BinaryTree(ToDictMixin):
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
# 이해를 돕기 위해
temp_node1 = BinaryTree(5, None, None)
print(temp_node1.__dict__)# {'value': 5, 'left': None, 'right': None}
# 이해를 돕기 위해
temp_node2 = BinaryTree(3, temp_node1, None)
print(temp_node2.__dict__)# {'value': 3, 'left': <__main__.BinaryTree object at 0x000002A5DA1D9730>, 'right': None}
# Example 4
tree = BinaryTree(10,
left=BinaryTree(7, right=BinaryTree(9)),
right=BinaryTree(13, left=BinaryTree(11)))
pprint.pprint(tree.to_dict())
# print
{'left': {'left': None,
'right': {'left': None, 'right': None, 'value': 9},
'value': 7},
'right': {'left': {'left': None, 'right': None, 'value': 11},
'right': None,
'value': 13},
'value': 10}
정리
- 믹스인을 사용해 구현할 수 있는 기능을 인스턴스 애트리뷰트와 __init__을 사용하는 다중 상속을 통해 구현하지 말라.
- 믹스인 클래스가 클래스별로 특화된 기능을 필요로 한다면 인스턴스 수준에서 끼워 넣을 수 있는 기능(정해진 메서드를 통해 해당 기능을 인스턴스가 제공하게 만듦)을 활용하라.
- 믹스인에는 필요에 따라 인스턴스 메서드는 물론 클래스 메서드도 포함될 수 있다.
- 믹스인을 합성하면 단순한 동작으로부터 더 복잡한 기능을 만들어낼 수 있다.
'python' 카테고리의 다른 글
이펙티브 파이썬 2nd 정리 #4 (0) | 2023.09.01 |
---|---|
이펙티브 파이썬 2nd 정리 #3 (0) | 2023.08.18 |
이펙티브 파이썬 2nd 정리 #2 (0) | 2023.08.05 |
이펙티브 파이썬 2nd 정리 #1 (1) | 2023.07.28 |