[Python]リストのようなものを作る

今回は，Pythonでリストのようなものを実装します．ここでリストのようなというのは以下の条件を満たすこととします．

1. インデックスにより要素を取り出せる(`__getitem__`を実装している)
1. for文に添えることができる(`__iter__`を実装している)
1. スライスを利用できる(`__getitem__`でスライスの処理を実装している)

具体的には，以下のように桁ごとに分割できる整数を実装していきます

```python
digit_devided_int = DigitDividedInt(1234567890)
print(digit_devided_int[3])
print(list(digit_devided_int))
```

    7
    [0, 9, 8, 7, 6, 5, 4, 3, 2, 1]
    


```python
for digit in digit_devided_int:
    print(digit)
```

    0
    9
    8
    7
    6
    5
    4
    3
    2
    1
    


```python
list(digit_devided_int[0:5:2])
```




    [0, 8, 6]


`DigitDividedInt`の定義は以下となります．数値を桁ごとに分割するには文字列に変換することでも実現できその方が効率的ですが，今回は文字列の変換を用いずに実装してみます．

```python
class DigitDividedInt:
    """
    桁ごとに分割するint
    """
    def __init__(self, _int):
        self._inner_int = _int
        self._digit_dict = {}
        
        digit_counter = 1
        while True:
            if _int < 10**(digit_counter+1):
                break
            digit_counter += 1
            
        self.max_digit = digit_counter  # 桁数(0からスタート)
                
        for i in list(range(digit_counter+1))[::-1]:  # 後ろから
            digit_number = _int // (10**i)

            self._digit_dict[str(10**i)] = digit_number
            _int = _int - digit_number * (10**i)

    def __int__(self):
        return int(self._inner_int)
    
    def __len__(self):
        return len(self._digit_dict.keys())
    
    def __getitem__(self, _slice):
        if isinstance(_slice, int):
            index = _slice
            return self._getitem_index(index)
        elif isinstance(_slice, str):
            key = _slice
            return self._getitem_key(key)
        elif isinstance(_slice, slice):
            return self._getitem_slice(_slice)
        else:
            raise Exception("Invalid arg type of __getitem__")
        
    def _getitem_index(self, index):
        if index > self.max_digit:
            raise IndexError("DigitDivideInt index out of range")
        return self._digit_dict[str(10**index)]
    
    
    def _getitem_key(self, key):
        if key not in self._digit_dict.keys():
            raise KeyError(str(key))
            
        return self._digit_dict[key]
        
    def _getitem_slice(self, _slice):
        start, stop, step = _slice.start, _slice.stop, _slice.step
        if (isinstance(start, int) or start is None) and  (isinstance(stop, int) or stop is None) and (isinstance(step, int) or step is None):
            if start is None:  # startがNoneの場合
                start = 0       
            if stop is None:  # stopがNoneの場合
                stop = len(self)
            if step is None:  # stepがNoneの場合
                step = 1
            
            for index in range(start, stop, step):
                yield self._getitem_index(index)
        elif (isinstance(start, str) or start is None) and (isinstance(stop, str) or stop is None) and (isinstance(step, int) or step is None):            
            key_list = list(self._digit_dict.keys())
            
            if (start not in key_list) or (stop not in key_list):  # start, stopがキーにあるかどうか
                raise Exception("start and stop of slicke must be in digit of int")
            
            int_key_list = map(int, key_list)  # 整数にしたキーのリスト
            
            if start is None:  # startがNoneの場合
                start = str(min(int_key_list))        
            if stop is None:  # stopがNoneの場合
                stop = str(max(int_key_list))
            if step is None:  # stepがNoneの場合
                step = 1
            
            # 範囲内のキーの取得とソート
            int_key_in_range = [int_key for int_key in int_key_list if (int(start) <= int_key) and (int_key < int(stop))]
            key_in_range = [str(key) for key in sorted(int_key_in_range)]
            
            for key in key_in_range[::step]:
                yield self._getitem_key(key)
        else:
            raise TypeError("slice types are invalid")
                        
    def __iter__(self):
        for index in range(len(self)):
            yield self[index]
            
    def __list__(self):
        return list(iter(self))
```

初期化メソッドでは桁とその値を対応させたディクショナリ`self._digit_dict`を作成しています．例えば，先ほどの例の`_digit_dict`はこのようになります．

```python
digit_devided_int._digit_dict
```




    {'1000000000': 1,
     '100000000': 2,
     '10000000': 3,
     '1000000': 4,
     '100000': 5,
     '10000': 6,
     '1000': 7,
     '100': 8,
     '10': 9,
     '1': 0}





そして条件1と3を満たすために特殊メソッド`__getitem__`を実装します．`__getimtem__`は`digit_devided_int[1]`のように添え字を与えたときに呼ばれます．
今回は添え字の型によって条件分岐してそれぞれメソッドを実行しています．おまけとして`'1000'`などの文字列を与えたときも辞書のように値を取得できます．

```python
def __getitem__(self, _slice):
    if isinstance(_slice, int):
        index = _slice
        return self._getitem_index(index)
    elif isinstance(_slice, str):
        key = _slice
        return self._getitem_key(key)
    elif isinstance(_slice, slice):
        return self._getitem_slice(_slice)
    else:
        raise Exception("Invalid arg type of __getitem__")
```

普通に整数のインデックスを添えた場合は`_getitem_index`が呼ばれます．`_getitem_index`メソッドではインデックスから対応するキーを作成し，`self._digit_dict`から値を取り出しています．

```python
def _getitem_index(self, index):
    if index > self.max_digit:
        raise IndexError("DigitDivideInt index out of range")
    return self._digit_dict[str(10**index)]
```

`digit_devided_int[0:8:3]`のようにスライスを添えたときに呼ばれるのが`_getitem_slice`メソッドです．スライスに数値を与えたときと文字列を与えたときの二つを実装していますが，今回は数値を与えたときを見ていきます．スライスは部分的に与えることが多いので，Noneの時も大丈夫なようにします．ここではスライスの処理を`range`に移譲させ`for`文で取り出しながら`yield`するジェネレーターを返します．これは取り出す処理の負荷が大きく一つ一つしか取り出せないような状況を想定したものであり，今回の例では普通にリストを返して問題ありません．

```python
def _getitem_slice(self, _slice):
    start, stop, step = _slice.start, _slice.stop, _slice.step
    if (isinstance(start, int) or start is None) and  (isinstance(stop, int) or stop is None) and (isinstance(step, int) or step is None):
        if start is None:  # startがNoneの場合
            start = 0       
        if stop is None:  # stopがNoneの場合
            stop = len(self)
        if step is None:  # stepがNoneの場合
            step = 1
        
        for index in range(start, stop, step):
            yield self._getitem_index(index)
    elif (isinstance(start, str) or start is None) and (isinstance(stop, str) or stop is None) and (isinstance(step, int) or step is None):            
        key_list = list(self._digit_dict.keys())
        
        if (start not in key_list) or (stop not in key_list):  # start, stopがキーにあるかどうか
            raise Exception("start and stop of slice must be in digit of int")
        
        int_key_list = map(int, key_list)  # 数値にしたキーのリスト
        
        if start is None:  # startがNoneの場合
            start = str(min(int_key_list))        
        if stop is None:  # stopがNoneの場合
            stop = str(max(int_key_list))
        if step is None:  # stepがNoneの場合
            step = 1
        
        # 範囲内のキーの取得とソート
        int_key_in_range = [int_key for int_key in int_key_list if (int(start) <= int_key) and (int_key < int(stop))]
        key_in_range = [str(key) for key in sorted(int_key_in_range)]
        
        for key in key_in_range[::step]:
            yield self._getitem_key(key)
    else:
        raise TypeError("slice types are invalid")
```

2の条件を満たすには，特殊メソッド`__iter__`を実装します．今回はリストを返しても問題ありませんが，取り出しの負荷が大きい場合を想定してジェネレーターを返しています．`len`関数も使えるように特殊メソッド`__len__`も実装しておきます．

```python
def __iter__(self):
    for index in range(len(self)):
        yield self[index]
```

## まとめ
リストのように

1. インデックスにより要素を取り出せる
1. for文に添えることができる
1. スライスを利用できる

の条件を満たすオブジェクトを作成しました．