【Python】カスタマイズしたlistクラスを作成する【UserList】
リストのように扱えるクラスを作成したい場合、いくつか方法があるが、collections.UserListを継承すると、基本的なlistで使えるメソッドが継承され、カスタマイズしたい部分のみをオーバーライドすることで、リストっぽいクラスを作成できる。
UserListを使う以外の方法として、collections.abc.MutableSequenceを継承する方法、何も継承せずに必要なメソッドなどを定義していく方法もあるが、どれを選ぶかはケースバイケース。
リストの動きをそのまま踏襲してOKな場合はUserListを使うといい。
UserListを継承してクラス作成
UserListはリストのラッパーのようなもの。
単純にUserListを継承すると、listと同じ動きになる。
1from collections import UserList
2
3# UserListを継承しただけのクラス作成
4class MyUserList(UserList):
5 pass
MyUserListの中身は何も書いていないが、listのように扱える。
1# 初期値なしでインスタンス生成
2my_user_list = MyUserList()
3my_user_list.append(1)
4print(my_user_list)
5# [1]
6my_user_list.append(2)
7print(my_user_list)
8# [1, 2]
9my_user_list.pop()
10print(my_user_list)
11# [1]
12
13# 初期値ありでインスタンス生成
14my_user_list = MyUserList([1, 2, 3, 4, 5])
15print(my_user_list)
16# [1, 2, 3, 4, 5]
17my_user_list.pop(0)
18print(my_user_list)
19# [2, 3, 4, 5]
20my_user_list.insert(0, 10)
21print(my_user_list)
22# [10, 2, 3, 4, 5]
UserListの内容
以下のコードの最後の方(1200行目ぐらい)にUserListが定義されている。
https://github.com/python/cpython/blob/3.11/Lib/collections/init.py
以下に、UserListを抜粋する。
UserListはMutableSequenceを継承しているため、MutableSequenceで使える抽象メソッドやMixinメソッドの動作が予め定義されている。
1class UserList(_collections_abc.MutableSequence):
2 """A more or less complete user-defined wrapper around list objects."""
3
4 def __init__(self, initlist=None):
5 self.data = []
6 if initlist is not None:
7 # XXX should this accept an arbitrary sequence?
8 if type(initlist) == type(self.data):
9 self.data[:] = initlist
10 elif isinstance(initlist, UserList):
11 self.data[:] = initlist.data[:]
12 else:
13 self.data = list(initlist)
14
15 def __repr__(self):
16 return repr(self.data)
17
18 def __lt__(self, other):
19 return self.data < self.__cast(other)
20
21 def __le__(self, other):
22 return self.data <= self.__cast(other)
23
24 def __eq__(self, other):
25 return self.data == self.__cast(other)
26
27 def __gt__(self, other):
28 return self.data > self.__cast(other)
29
30 def __ge__(self, other):
31 return self.data >= self.__cast(other)
32
33 def __cast(self, other):
34 return other.data if isinstance(other, UserList) else other
35
36 def __contains__(self, item):
37 return item in self.data
38
39 def __len__(self):
40 return len(self.data)
41
42 def __getitem__(self, i):
43 if isinstance(i, slice):
44 return self.__class__(self.data[i])
45 else:
46 return self.data[i]
47
48 def __setitem__(self, i, item):
49 self.data[i] = item
50
51 def __delitem__(self, i):
52 del self.data[i]
53
54 def __add__(self, other):
55 if isinstance(other, UserList):
56 return self.__class__(self.data + other.data)
57 elif isinstance(other, type(self.data)):
58 return self.__class__(self.data + other)
59 return self.__class__(self.data + list(other))
60
61 def __radd__(self, other):
62 if isinstance(other, UserList):
63 return self.__class__(other.data + self.data)
64 elif isinstance(other, type(self.data)):
65 return self.__class__(other + self.data)
66 return self.__class__(list(other) + self.data)
67
68 def __iadd__(self, other):
69 if isinstance(other, UserList):
70 self.data += other.data
71 elif isinstance(other, type(self.data)):
72 self.data += other
73 else:
74 self.data += list(other)
75 return self
76
77 def __mul__(self, n):
78 return self.__class__(self.data * n)
79
80 __rmul__ = __mul__
81
82 def __imul__(self, n):
83 self.data *= n
84 return self
85
86 def __copy__(self):
87 inst = self.__class__.__new__(self.__class__)
88 inst.__dict__.update(self.__dict__)
89 # Create a copy and avoid triggering descriptors
90 inst.__dict__["data"] = self.__dict__["data"][:]
91 return inst
92
93 def append(self, item):
94 self.data.append(item)
95
96 def insert(self, i, item):
97 self.data.insert(i, item)
98
99 def pop(self, i=-1):
100 return self.data.pop(i)
101
102 def remove(self, item):
103 self.data.remove(item)
104
105 def clear(self):
106 self.data.clear()
107
108 def copy(self):
109 return self.__class__(self)
110
111 def count(self, item):
112 return self.data.count(item)
113
114 def index(self, item, *args):
115 return self.data.index(item, *args)
116
117 def reverse(self):
118 self.data.reverse()
119
120 def sort(self, /, *args, **kwds):
121 self.data.sort(*args, **kwds)
122
123 def extend(self, other):
124 if isinstance(other, UserList):
125 self.data.extend(other.data)
126 else:
127 self.data.extend(other)
こんな感じで、listで使えるメソッドなどが一通り定義されているため、継承して何も書かなくてもlistのように動作する。
必要に応じて、__init__や__eq__、__lt__などをオーバーライドしてカスタマイズしていく。
UserListのカスタマイズ例
int型の値のみを許容するリスト
例としてint型の値のみを許容するリストクラスを作成する。
int型以外の値を設定、追加しようとするとエラーとする。
__init__だけをオーバーライドした時の挙動や、値追加系メソッドもオーバーライドした時の挙動を見ていく。
__init__だけをオーバーライドした場合
int型以外の値が入ってきた場合はエラーとする。
1class MyIntUserList(UserList):
2 def __init__(self, init_list=None):
3 self.data = []
4 if init_list is not None:
5 if type(init_list) == type(self.data):
6 # 全ての値がint型の場合のみ値を設定する
7 for v in init_list:
8 if not isinstance(v, int):
9 raise TypeError(f"MyIntUserList requires list[int]")
10 self.data[:] = init_list
11 # MyUserListの場合は値がintであることが保証されているので、そのまま代入する
12 elif isinstance(init_list, "MyIntUserList"):
13 # リストなので、コピーを代入する
14 self.data[:] = init_list.data[:]
15 else:
16 raise TypeError(f"MyIntList requires list[int]")
インスタンス生成時にint以外の値があるとエラーとなる。
1# 初期値にint型以外を入れるとエラーとなる
2MyIntUserList([1, 2, 3.3])
3# TypeError: MyIntUserList requires list[int]
しかし、appendなどのメソッドは通常のlistのままなので、int型以外でも追加可能になる。
1# 初期値のリストの値がint型のみであればMyIntUserListを作成できる
2my_int_user_list = MyIntUserList([1, 2, 3])
3# ただし、appendなどのメソッドはオーバーライドしていないので、どんな値でも追加できてしまう。
4my_int_user_list.append(3.3)
5print(my_int_user_list)
6# [1, 2, 3, 3.3]
値追加に関係するメソッドをオーバーライドする場合
__setitem__, append, insert, extendあたりをオーバーライドしてint型以外はエラーとする制約を入れる。
各種メソッドのオーバーライド
1class MyIntUserList(UserList):
2 def __init__(self, init_list=None):
3 self.data = []
4 if init_list is not None:
5 if type(init_list) == type(self.data):
6 # 全ての値がint型の場合のみ値を設定する
7 for v in init_list:
8 if not isinstance(v, int):
9 raise TypeError(f"MyIntUserList requires list[int]")
10 self.data[:] = init_list
11 # MyIntUserListの場合は値がintであることが保証されているので、そのまま代入する
12 elif isinstance(init_list, "MyIntUserList"):
13 # リストなので、コピーを代入する
14 self.data[:] = init_list.data[:]
15 else:
16 raise TypeError(f"MyIntList requires list[int]")
17
18 def __setitem__(self, i, item):
19 if isinstance(item, int):
20 self.data[i] = item
21 else:
22 raise TypeError(f"MyIntUserList requires data of type int")
23
24 def append(self, item):
25 if isinstance(item, int):
26 self.data.append(item)
27 else:
28 raise TypeError(f"MyIntUserList requires data of type int")
29
30 def insert(self, i, item):
31 if isinstance(item, int):
32 self.data.insert(i, item)
33 else:
34 raise TypeError(f"MyIntUserList requires data of type int")
35
36 def extend(self, other):
37 if isinstance(other, MyIntUserList):
38 self.data.extend(other.data)
39 elif isinstance(other, list):
40 for v in other:
41 if not isinstance(v, int):
42 raise TypeError(f"MyIntUserList requires list[int]")
43 self.data.extend(other)
44 else:
45 raise TypeError(f"MyIntUserList requires list[int]")
int型以外を追加しようとするとエラーとなることの確認
appendなどでint型以外の値を追加しようとするとエラーとなる。
1# 初期値のリストの値がint型のみであればMyIntUserListを作成できる
2my_int_user_list = MyIntUserList([1, 2, 3])
3
4# index指定でint型以外の値は代入できない(__setitem__をオーバーライド済み)
5my_int_user_list[-1] = 3.3
6# TypeError: MyIntUserList requires data of type int
7
8# appendでint型以外の値は追加できない
9my_int_user_list.append(3.3)
10# TypeError: MyIntUserList requires data of type int
11
12# inseetでint型以外の値は挿入できない
13my_int_user_list.insert(2, 3.3)
14# TypeError: MyIntUserList requires data of type int
15
16# extendでint型以外が含まれるリストの拡張はできない
17my_int_user_list.extend([3.3])
18# TypeError: MyIntUserList requires list[int]
int型は追加可能
1my_int_user_list = MyIntUserList([1, 2, 3])
2my_int_user_list.append(4)
3print(my_int_user_list)
4# [1, 2, 3, 4]
5my_int_user_list.insert(0, 5)
6print(my_int_user_list)
7# [5, 1, 2, 3, 4]
8my_int_user_list.extend([10, 20])
9print(my_int_user_list)
10# [5, 1, 2, 3, 4, 10, 20]
11my_int_user_list[-1] = 30
12print(my_int_user_list)
13# [5, 1, 2, 3, 4, 10, 30]
__add__系も必要に応じてオーバーライド
__iadd__は自分自身を更新するため、今回の例の場合はオーバーライドが必要となる。
__add__, __radd__は「+」の仕様をどのようにしたいかでオーバーライドした方がいいのかが変わってくる。
まとめ
UserListを継承すると、手軽にlistクラスをカスタマイズできる。
1from collections import UserList
2
3class MyUserList(UserList):
4 def __init__(self, init_list):
5 do_something
6
7 def __getitem__(self, i):
8 do_something
9
10 def __setitem__(self, i, item):
11 do_something
12 # 必要に応じてメソッドをオーバーライド
13
例ではint型のみを許容するクラスを作成したが、プリミティブ型以外の値を持たせる場合、比較演算子やソートなども意図した動作になるようにオーバーライドしていく必要がある。
オーバーライドする量が多い場合、わざわざ継承しなくても、必要なメソッドを随時追加すれば良い気もするが、User Listを継承しておけば、このクラスはlistっぽく扱うということが伝わりやすいと言う利点もある。
抽象メソッドのオーバーライドができるようになってくると、自作のクラスを配列のように扱えるなど、やれることの幅が広がる。