【Python】collections.UserListを使って独自のリスト風クラスを実装する

pythonの`collections`について調べていたところ、`collections.UserList`について書かれている記事がほぼなかったため、自分なりに使い方をまとめてみました。

※実行環境
python3.7.5

##`collections.UserList`とは

[公式ドキュメント](https://docs.python.org/ja/3.7/library/collections.html#collections.UserList)には次のように書いてあります。

>
このクラスはリストオブジェクトのラッパとしてはたらきます。これは独自のリスト風クラスの基底クラスとして便利で、既存のメソッドをオーバーライドしたり新しいメソッドを加えたりできます。こうして、リストに新しい振る舞いを加えられます。

上記の通り、`UserList`はリスト風なクラスを自作するときのベースクラスです。リスト風クラスとは、`append`や`remove`ができたり、インデクサをもつクラスですね。

##`collections.UserList`を使ってみる

例として、リストをコンストラクタ時の初期状態に戻す`reset`メソッドを持つリスト`ResettableList`を実装してみましょう。

```python:
from collections import UserList


# collections.UserListを継承
class ResettableList(UserList):
    def __init__(self, initlist=None):
        super().__init__(initlist)
        self._initlist = initlist.copy()  # initlistのコピーを保持しておく

    def reset(self):
        self.data = self._initlist.copy()  # self.dataはラップされているリスト


mylist = ResettableList([1, 2, 3])  # 初期リストをコンストラクタにわたす
print(f'mylist = {mylist}')
print(f'mylist.data = {mylist.data}')  # [非推奨]dataで内部のリストにアクセスできる

# 通常のlistのような操作ができる
print(f'mylist[1] = {mylist[1]}')
mylist.append(4)
print(f'mylist = {mylist}')
mylist.remove(1)
print(f'mylist = {mylist}')

mylist.reset()  # 初期状態に戻す
print(f'mylist = {mylist}')
```

結果

```
mylist = [1, 2, 3]
mylist.data = [1, 2, 3]
mylist[1] = 2
mylist = [1, 2, 3, 4]
mylist = [2, 3, 4]
mylist = [1, 2, 3]
```

`ResettableList`はラッパーであり、メンバ変数`data`にリストを格納しています。独自の振る舞いである`reset`メソッドは、この`self.data`を操作して実装しています。
また、例では外部からメンバ変数`data`にアクセスしていますが、予期せぬ不具合をうむ可能性があるため、この行為は避けるべきです。

これを示すために、要素の削除ができないリスト`UnremovableList`を実装しました。

```python:
from collections import UserList


# 要素を削除するメソッドが使用されたときの例外
class RemoveError(Exception):
    pass


class UnremovableList(UserList):
    # 要素を削除するメソッドをオーバーライドし、例外を発生させる
    # 簡略化のため、ここではremoveのみオーバーライドする
    def remove(self, item):
        raise RemoveError('Cannot remove')


mylist = UnremovableList([1, 2, 3])
print(f'mylist = {mylist}')

# removeできない
try:
    mylist.remove(1)
except RemoveError as e:
    print(f'RemoveError: {e}')
print(f'mylist = {mylist}')

# mylist.dataにアクセスするとremoveできてしまう
mylist.data.remove(1)
print(f'mylist = {mylist}')
```

結果

```
mylist = [1, 2, 3]
RemoveError: Cannot remove
mylist = [1, 2, 3]
mylist = [2, 3]
```

このように、外部から`data`にアクセスしたことで、禁止したはずの要素削除ができてしまいます。

##`list`を継承でもいいのでは

リスト風クラスを作るのであれば、`list`を継承してもよいはずです。`list`ではなくわざわざ`collections.UserList`を継承させるメリットはあるのでしょうか？
[公式ドキュメント](https://docs.python.org/ja/3.7/library/collections.html#collections.UserList)には、以下のように書いてあります。

>
このクラスの必要性は、 list から直接的にサブクラス化できる能力に部分的に取って代わられました; しかし、根底のリストに属性としてアクセスできるので、このクラスを使った方が簡単になることもあります。

「このクラス」とは、`UserList`のことです。要約すると、「`list`を継承してもよいが、`UserList`を継承した方が簡単になる。」になります。

ためしに、`list`を継承したらどんな実装になるのか見てみましょう。先程の例の`ResettableList`をこの方法で実装します。

```python:
class ResettableList(list):
    def __init__(self, initlist=None):
        super().__init__(initlist)
        self._initlist = initlist.copy()

    def reset(self):
        self.clear()
        self.extend(self._initlist)
```

`UserList`を継承したときと異なり、自分自身のインスタンスを操作しなければなりません。そこで、一度リストを空に、初期リストを追加する方法で実装しました。
※もっと楽な実装あれば教えてください。

両者を比較すると、次のようになります。（`reset`メソッドのみ記載）

```python:
#UserList継承バージョン
class ResettableList(UserList):
    def reset(self):
        self.data = self._initlist.copy()


#list継承バージョン
class ResettableList(list):
    def reset(self):
        self.clear()
        self.extend(self._initlist)
```

`UserList`を継承した方がシンプルです。

##おわりに
以上、`collections.UserList`の使い方を自分なりにまとめてみました。
`UserList`は内部にリストを持ちながら、外部から見るとリストそのものに見えるという構造になっています。これは、一般的なクラスを拡張するときにも参考になると思います。

ちなみに、`collections`には`UserList`と似た`UserDict`、`UserString`もあります。それぞれ`dict`、`str`風なクラスを作るためのベースクラスです。
これらのクラスを使う頻度は少ないと思いますが、ここぞいうう場面で適切に使っていきたいですね。