Home [Better Way #43] 커스텀 컨테이너 타입은 collections.abc를 상속하라
Post
Cancel

[Better Way #43] 커스텀 컨테이너 타입은 collections.abc를 상속하라

본문은 “파이썬 코딩의 기술 (Effective Python, 2판)”“Chapter 05. Classes and Interfaces”을 읽고 정리한 내용입니다.


파이썬 컨테이너 타입을 이용하여 커스텀 컨테이너 타입을 만들고 싶다면 다음의 두 가지 방법 중 하나를 사용한다.

  1. 간편하게 사용할 경우에는 파이썬 컨테이너 타입(ex. list, dict)을 직접 상속하여 구현한다.
  2. 커스텀 컨테이너를 제대로 구현하려면 수많은 메서드를 구현해야 하므로 collection.abc에 정의된 인터페이스를 상속하여 구현한다.

    커스텀 컨테이너 타입이 정상적으로 작동하기 위해 필요한 인터페이스기능을 제대로 구현하도록 보장할 수 있다!


그렇다면, 왜 커스텀 컨테이너를 제대로 구현하기 위해서는 collection.abc를 상속해야 하는지 시퀀스 타입을 예시로 알아보자!


파이썬 컨테이너 타입을 직접 상속받아 커스텀 컨테이터 타입 정의 시 주의할 점

  1. 커스텀 컨테이너 타입을 정의할 때, 작성하는 클래스가 올바른 컨테이너 타입이 되려면 특정 메서드를 반드시 구현해야 한다.

    커스텀 시퀀스 타입의 경우, 특별 메서드 __len____getitem__을 구현해야 한다.

  2. 필요한 특정 메서드를 모두 구현했더라도, 파이썬 프로그래머가 리스트 인스턴스에서 기대할 수 있는 모든 시퀀스의 의미 구조를 제공할 수 없을 수도 있다.

    커스텀 시퀀스 타입을 구현하기 위해 __len____getitem__을 구현했더라도, 이것으로는 충분하지 않다. 파이썬 프로그래머가 시퀀스 타입에 있을 것이라고 예상하는 countindex 메서드가 없기 때문이다!


이러한 이유로 파이썬 컨테이너 타입을 직접 상속받아 커스텀 컨테이너 타입을 직접 정의하는 것은 생각보다 훨씬 더 어렵다.

collections.abc 모듈을 사용한다면 이를 어떻게 해결할 수 있을까?


커스텀 컨테이너 타입 정의 시 collections.abc 모듈을 사용하는 이유

  1. collections.abc 모듈은 추상 기반 클래스 정의를 제공한다.

    추상 기반 클래스는 컨테이너 타입에 정의해야 하는 전형적인 메서드를 모두 제공하므로, 하위 클래스 작성 시 이 중 필요한 메서드의 구현을 잊어버리면 에러를 통해 실수한 부분을 알 수 있다.

    1
    2
    3
    4
    
     from collections.abc import Sequence
        
     class BadType(Sequence):
         pass
    
    1
    2
    
     foo = BadType()
     # TypeError: Can't instantiate abstract class BadType with abstract methods __getitem__, __len__
    
  2. 추상 기반 클래스가 요구하는 모든 메서드를 구현하면, 그 외의 추가 메서드 구현을 상속 받아 자연스럽게 얻을 수 있다.

    커스텀 시퀀스 타입의 경우, 파이썬 프로그래머가 시퀀스 타입에 있을 것이라고 예상하는 countindex 메서드와 같은 추가 메서드의 구현을 따로 하지 않아도 된다!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
     class BetterNode(Sequence):
         def __init__(self, value, left=None, right=None):
             self.value = value
             self.left = left
             self.right = right
            
         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}")
            
         def __len__(self):
             for count, _ in enumerate(self._traverse(), 1):
                 pass
             return count
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
     tree = BetterNode(
         10,
         left=BetterNode(
             5,
             left=BetterNode(2),
             right=BetterNode(
                 6,
                 right=BetterNode(7)
             )
         ),
         right=BetterNode(
             15,
             left=BetterNode(11)
         )
     )
    
    1
    2
    3
    4
    5
    6
    
     print("트리:", list(tree))
     print("7의 인덱스:", tree.index(7))
     print("10의 개수:", tree.count(10))
     # 트리: [2, 5, 6, 7, 10, 11, 15]
     # 7의 인덱스: 3
     # 10의 개수: 1
    


따라서 Set이나 MutableMapping과 같이 파이썬의 관례에 맞춰 구현해야 하는 특별 메서드가 훨씬 많은 더 복잡한 컨테이너 타입을 구현할 때는 더욱 더 collections.abc의 이점이 커진다.

This post is licensed under CC BY 4.0 by the author.

[Better Way #42] 비공개 애트리뷰트보다는 공개 애트리뷰트를 사용하라

[OOP] 객체지향의 장점과 특징