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
風なクラスを作るためのベースクラスです。
これらのクラスを使う頻度は少ないと思いますが、ここぞいうう場面で適切に使っていきたいですね。