Python

【Python】カスタマイズしたlistクラスを作成する【UserList】

MAX

リストのように扱えるクラスを作成したい場合、いくつか方法があるが、collections.UserListを継承すると、基本的なlistで使えるメソッドが継承され、カスタマイズしたい部分のみをオーバーライドすることで、リストっぽいクラスを作成できる。

UserListを使う以外の方法として、collections.abc.MutableSequenceを継承する方法、何も継承せずに必要なメソッドなどを定義していく方法もあるが、どれを選ぶかはケースバイケース。

リストの動きをそのまま踏襲してOKな場合はUserListを使うといい。

スポンサーリンク

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っぽく扱うということが伝わりやすいと言う利点もある。

抽象メソッドのオーバーライドができるようになってくると、自作のクラスを配列のように扱えるなど、やれることの幅が広がる。

スポンサーリンク
ABOUT ME
MAX
MAX
ITエンジニア、データサイエンティスト
新卒でSIerに入社し、フリーランスになってWEB系へ転向。
その後AIの世界へ足を踏み入れ、正社員に戻る。 テーブルデータの分析がメイン。
スポンサーリンク
記事URLをコピーしました