파이썬 코딩의 기술 - 43

JinWooHyun·2021년 8월 11일
0

파이썬 코딩의 기술

목록 보기
13/14

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

모든 파이썬 클래스는 함수와 애트리뷰트를 함께 캡슐화하는 일종의 컨테이너라고 할 수 있다. 파이썬은 데이터를 관리할 때 사용할 수 있도록 list, tuple, set, dict 등 내장 컨테이너 타입을 제공한다.

시퀀스처럼 사용법이 간단한 클래스를 정의할 때는 파이썬 내장 리스트 타입의 하위 클래스를 만들고 싶은 것이 당연하다. 예를 들어 멤버들의 빈도를 계산하는 메서드가 포함된 커스텀 리스트 타입이 필요하다고 가정하자.

class FrequencyList(list):
    def __init__(self, members):
        self.members = members
    
    def frequency(self):
        counts = {}
        for item in self:
            counts[item] = counts.get(item, 0) + 1
        return counts

FrequencyList를 리스트의 하위 클래스로 만듦으로써 리스트가 제공하는 모든 표준 함수를 사용할 수 있으며, 필요한 기능을 제공하는 메서드를 얼마든지 추가할 수 있다.

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}

이제 리스트처럼 느껴지면서 인덱싱이 가능한 객체를 제공하고 싶은데, 리스트의 하위 클래스로 만들고 싶지는 않다고 가정해보자. 다음 이진 트리 클래스를 시퀀스(리스트나 튜플)의 의미 구조를 사용해 다룰 수 있는 클래스를 만들고 싶다.

class BinaryNode:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

어떻게 이 클래스가 시퀀스 타입처럼 작동하게 할 수 있을까? 파이썬에서는 특별한 이름의 인스턴스 메서드를 사용해 컨테이너의 동작을 구현한다.

bar = [1,2,3]
bar[0]

=> bar.__getitem__(0)

위처럼 인덱스를 사용해 시퀀스에 접근하는 코드는 __getitem__ 특별 메서드로 해석된다.

BinaryNode 클래스가 시퀀스처럼 작동하게 하려면 트리 노드를 깊이 우선 순회(depth first traverse)하는 커스텀 __getitem__ 메서드 구현을 제공하면 된다.

__getitem 을 double underscore getitem 줄여서 dunder getitem이라고 부르는 경우가 많다.

class IndexableNode(BinaryNode):
    def _traverse(self):
        if self.left is not None:
            yield from self.left._traverse()
        yield self
        if self.right is not None:
            yield from self.right._traverse()
        
    def __getitem__(self, index):
        for i, item in enumerate(self._traverse()):
            if i == index:
                return item.value
       raise IndexError(f'인덱스 범위 초과: {index}')
tree = IndexableNode(
    10,
    left=IndexableNode(
        5,
        left=IndexableNode(2),
        right=IndexableNode(
            6,
            right=IndexableNode(7))),
    right=IndexableNode(
        15,
        left=IndexableNode(11)))


>>>
        10
    5        15
  2   6    11   X
 X X X 7  X  X

이 트리를 leftright 애트리뷰트를 사용해 순회할 수도 있지만, 추가로 리스트처럼 접근할 수 있다.

print('LRR:', tree.left.right.right.value)
print('인덱스 0:', tree[0])
print('인덱스 1:', tree[1])
print('11이 트리 안에 있나?', 11 in tree)
print('17이 트리 안에 있나?', 17 in tree)
print('트리:', list(tree))

>>>
LRR: 7
인덱스 0: 2,
인덱스 1: 5,
11이 트리 안에 있나? True
17이 트리 안에 있나? False
트리: [2 ,5, 6, 7, 10, 11, 15]

문제는 __getitem__을 구현하는 것만으로는 리스트 인스턴스에서 기대할 수 있는 모든 시퀀스 의미 구조를 제공할 수 없다는 데 있다.

len(tree)

>>
Error!

len 내장 함수는 __len__이라는 이름의 특별 메서드를 구현해야 제대로 작동한다. 커스텀 시퀀스 타입은 이 메서드를 꼭 구현해야 한다.

class SequenceNode(IndexableNode):
    def __len(self):
        for count, _ in enumerate(self._traverse(), 1)
            pass
        return count
        
tree = SequenceNode(
    10,
    left=SequenceNode(
        5,
        left=SequenceNode(2),
        right=SequenceNode(
            6,
            right=SequenceNode(7))),
    right=SequenceNode(
        15,
        left=SequenceNode(11)))
        
print('트리 길이:', len(tree))

>>>
트리 길이: 7

안타깝지만 어떤 클래스가 올바른 시퀀스가 되려면 두 메서드(__getitem____len__)을 구현하는 것만으로는 충분하지 않다. (count, index 등등..)

파이썬을 사용할 때 흔히 발생하는 이런 문제를 해결하기 위해 내장 collections.abc 모듈 안에는 컨테이너 타입에 정의해야 하는 전형적인 메서드를 모두 제공하는 추상 기반 클래스 정의가 여러 가지 들어 있다. 이런 추상 기반 클래스의 하위 클래스를 만들고 필요한 메서드 구현을 잊어버리면, collections.abc 모듈이 실수한 부분을 알려준다.

from collections.abc import Sequence

class BadType(Sequence):
    pass

foo = BadType()

>>
Traceback ...
TypeError: Can't instaniate abstract class BadType with
abstract methods __getitem__, __len__ ..

SequenceNode에서 한 것처럼 collections.abc에서 가져온 추상 기반 클래스가 요구하는 모든 메서드를 구현하면 indexcount와 같은 추가 메서드 구현을 같이 얻을 수 있다.

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

Set이나 MutableMapping과 같이 파이썬의 관례에 맞춰 구현해야 하는 특별 메서드가 훨씬 많은 더 복잡한 컨테이너 타입을 구현할 때는 이런 추상 기반 클래스가 주는 이점이 더 커진다.

collections.abc 모듈 외에도, 객체 비교와 정렬을 위해 사용하는 다양한 특별 메서드가 있다. 컨테이너 클래스나 비컨테이너 클래스에서 모두 이런 특별 메서드를 구현할 수 있다.

기억해야 할 내용

  • 간편하게 사용할 경우에는 파이썬 컨테이너 타입(리스트나 딕셔너리 등)을 직접 상속하라.
  • 커스텀 컨테이너를 제대로 구현하려면 수많은 메서들를 구현해야 한다.
  • 커스텀 컨테이너 타입이 collections.abc에 정의된 인터페이스를 상속하면 커스텀 컨테이너 타입이 정상적으로 작동하기 위해 필요한 인터페이스와 기능을 제대로 구현하도록 보장할 수 있다.
    
profile
Unicorn Developer

0개의 댓글