python

이펙티브 파이썬 2nd 정리 #5

기기디 2023. 9. 8. 15:50

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