pythonのcollectionsについて調べていたところ、collections.UserListについて書かれている記事がほぼなかったため、自分なりに使い方をまとめてみました。
※実行環境
python3.7.5
collections.UserListとは
公式ドキュメントには次のように書いてあります。
このクラスはリストオブジェクトのラッパとしてはたらきます。これは独自のリスト風クラスの基底クラスとして便利で、既存のメソッドをオーバーライドしたり新しいメソッドを加えたりできます。こうして、リストに新しい振る舞いを加えられます。
上記の通り、UserListはリスト風なクラスを自作するときのベースクラスです。リスト風クラスとは、appendやremoveができたり、インデクサをもつクラスですね。
collections.UserListを使ってみる
例として、リストをコンストラクタ時の初期状態に戻すresetメソッドを持つリストResettableListを実装してみましょう。
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を実装しました。
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を継承させるメリットはあるのでしょうか?
公式ドキュメントには、以下のように書いてあります。
このクラスの必要性は、 list から直接的にサブクラス化できる能力に部分的に取って代わられました; しかし、根底のリストに属性としてアクセスできるので、このクラスを使った方が簡単になることもあります。
「このクラス」とは、UserListのことです。要約すると、「listを継承してもよいが、UserListを継承した方が簡単になる。」になります。
ためしに、listを継承したらどんな実装になるのか見てみましょう。先程の例のResettableListをこの方法で実装します。
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メソッドのみ記載)
# 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風なクラスを作るためのベースクラスです。
これらのクラスを使う頻度は少ないと思いますが、ここぞいうう場面で適切に使っていきたいですね。