目次と前回の記事
Python のバージョンとこれまでに作成したモジュール
本記事のプログラムは Python の バージョン 3.13 で実行しています。
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
mbtest.py | テストに関する関数 |
util.py | ユーティリティ関数の定義 |
tree.py | ゲーム木に関する Node、Mbtree クラスなどの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
Array を利用したゲーム盤のデータ構造
これまでは list を利用してゲーム盤のデータ構造を表現してきましたが、list と似た array を利用してゲーム盤のデータ構造を表現 する方法を紹介します。
本記事での list、array、リスト、配列の用語の使い分け
array という用語は 配列 と 訳される場合が多い ので、日本語の文章では Python の array を配列と表記する場合が多いのではないかと思いますが、後で説明するように リストや配列という用語 の意味がプログラム言語によって 異なる場合がある ので、用語の意味を明確にする ために本記事では下記の表のように 用語を使い分ける ことにします。
用語 | 意味 |
---|---|
list と array | Python のデータ型 |
リストと配列 | 特定の性質を持つデータ構造の総称 プログラム言語によって具体的な意味や性質が異なる場合がある |
Python の list と array の違い
Python の list と array はどちらも 0 以上の整数 のインデックスを利用して 複数のデータを扱う データ構造であるという点では似ていますが、以下の点が異なります。
- list は 任意のデータ型 を要素とすることができる
- array は 特定のデータ型のみ を要素とする
list が任意のデータ型を要素に代入することができる理由は、以前の記事で説明したように list の要素 が 別々のオブジェクトで表される からです。

一方、Python の array は下図のように 複数の同じデータ型のデータ を 1 つのオブジェクトの中に記録します。

Python の array には下記のような 利点 があります。
- 同じ種類のデータを 1 つのオブジェクトで記録するため、list と比較して同じデータを扱う場合の メモリの消費量が少なくなる
- すべての要素の データ型が統一される ため、間違って異なるデータ型のデータを代入する ことによる バグを防ぐ ことができる
- array の要素 は メモリの連続した場所に記録され るので、データを コピーする処理 などを list よりも 高速に行うことができる
一方、Python の array は list と比較すると 要素 の 参照と代入処理が遅い という 欠点がある ようです。上記の利点と欠点については後で実際に検証します。
プログラム用語としてのリストと配列
プログラム用語としてのリスト は、順序がある複数のデータを扱う ことができる データ構造の総称1 として使われることが多いのではないかと思います。リスト の主な実装方法には 連結リスト と 動的配列 があり、どちらの方法で実装されたかによって下記のノートのようにリストの性質が大きく異なります。なお、特に何の説明もなくリストという用語が用いられる場合は、連結リストの事を指す場合が多いのではないかと思います。
連結リスト(linked list)で実装された場合は 要素の追加と削除の処理が速い、要素の 参照と代入処理が遅い、データの量が多い という性質を持ちます。一方、動的配列2 で実装された場合は逆に 要素の参照と代入処理が速い、要素の追加と削除の処理が遅い、データの量が少ない という性質を持ちます。詳細は Wikipediaを参照して下さい。なお、Python のドキュメントから、Python の list は配列に似た仕組みを使って実装されており、要素の参照と代入処理を高速に行うことができるようです。
参考までに リスト、連結リスト、動的配列 の Wikipedia のリンクを下記に示します。
プログラム用語としての配列 は整数のインデックスを利用して 複数の同一のデータ型のデータ を 連続したメモリに記録 することで実装するのが 一般的 だと思いますが、Javascript のように Python の list と同様に 異なるデータを扱う ことができるデータ構造のことを Array3 と呼ぶプログラム言語もある ようです。従って、Python の list と JavaScript の Array は名前は異なりますが、その性質はかなり似ています。
参考までに 配列 の Wikipedia のリンクを下記に示します。
多くのプログラム言語 では 組み込みデータ型 として リストや配列を利用 できますが、上記の例のようにプログラム言語によって その性質が異なる場合があります。別のプログラム言語を学ぶ際 にはその点に 注意が必要 です。
Python の array の利用方法
Python の array は array という組み込みモジュールで定義 されています。
array モジュールの詳細は下記のリンク先を参照して下さい。
array の作成
array は下記のプログラムのように、array モジュールで定義された array というクラス の インスタンスを作成 することで作成します。その際に array に格納する データ型を表す文字列 を 最初の実引数に記述 します。"i"
は 整数型を表す文字列 で、作成した array を print
で表示すると実行結果のように 括弧の中に array のデータ型を表す文字列が表示 されます。
from array import array
a = array("i")
print(a)
実行結果
array('i')
下記は array が扱うことができる 主なデータ型を表す文字列 の一覧です。
文字列 | 意味 |
---|---|
i |
整数型。int 型に相当する。ただし、int 型と異なり扱える値の範囲には制限がある |
f |
浮動小数点型。float 型に相当する |
w |
文字。1 つの要素に代入できるのは 1 文字だけ |
上記のデータ型を指定した際の データの制限の詳細 や、他のデータ型 を表す主な文字列については、下記のリンク先を参照して下さい。
また、下記のプログラムのように 2 つ目の実引数 に list などの 反復可能オブジェクトを記述 することで、その要素が代入された array を作成 することができ、print
で表示すると実行結果のように () の中の データ型を表す文字列の後 に 要素の値の一覧が表示 されます。
a = array("i", [1, 2, 3])
print(a)
実行結果
array('i', [1, 2, 3])
要素の参照、代入、追加
array は下記のプログラムのように、list と同様の方法 でインデックスを記述して 要素を参照、代入 することができます。
print(a[0])
a[0] = 5
print(a[0])
print(a)
1
5
array('i', [5, 2, 3])
また、下記のプログラムのように append
メソッド で 要素を追加 することができます。プログラムは示しませんが pop
などのミュータブルなシーケンス型で利用できるメソッドを利用することもできます。
a.append(10)
print(a)
実行結果
array('i', [5, 2, 3, 10])
ただし、下記のプログラムのように array を作成するときに指定したデータ型と 異なるデータ型 のデータを 代入しようとすると TypeError が発生 します。
a[0] = "x"
実行結果
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[5], line 1
----> 1 a[0] = "x"
TypeError: 'str' object cannot be interpreted as an integer
array を利用したゲーム盤を表すクラスの定義
array は 特定のデータ型のデータのみを扱う点を除く と、list とほぼ同じ方法で処理を行う ことができます。従って、ゲーム盤を表すデータ構造 を list から array に入れ替え ても、ポリモーフィズム によって Marubatsu クラスなどのプログラムがそのまま動作 します。
ただし、array は 添字を 2 つ記述 することで 要素の参照や代入 を行うという 2 次元の array を扱うことはできません4。そのため、array を利用して ゲーム盤を表現 する場合は、1 次元の list の場合と同様 に 1 つの添字で座標を表現 する必要があります。
下記は array を利用してゲーム盤を表現する ArrayBoard クラスの定義 です。データ構造を 1 次元の list から array に変更 する以外は List1dBoard クラスと同じ処理を行う ので List1dBoard クラスを継承 し、ゲーム盤のデータを board
属性に代入 する処理を行う __init__
メソッドのみを定義 しています。
-
8 行目:1 次元の list でゲーム盤を表現する場合と同じ要素を持つ、文字列型のデータを扱う array を
board
属性に代入するように修正した。最初の実引数に記述する"w"
は、array の要素に 文字列型のデータ を代入することを表す。なお、"w"
を指定した場合 の array の 要素には 1 文字分の文字列のデータ しか代入できないが、マスの値を 1 文字の文字列で表現 しているので問題は発生しない
1 from array import array
2 from marubatsu import Marubatsu, List1dBoard
3
4 class ArrayBoard(List1dBoard):
5 def __init__(self, board_size=3, count_linemark=False):
6 self.BOARD_SIZE = board_size
7 self.count_linemark = count_linemark
8 self.board = array("w", [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2))
元と同じなので省略
行番号のないプログラム
from marubatsu import Marubatsu, List1dBoard
class ArrayBoard(List1dBoard):
def __init__(self, board_size=3, count_linemark=False):
self.BOARD_SIZE = board_size
self.count_linemark = count_linemark
self.board = array("w", [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2))
if self.count_linemark:
self.rowcount = {
Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
Marubatsu.CROSS: [0] * self.BOARD_SIZE,
}
self.colcount = {
Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
Marubatsu.CROSS: [0] * self.BOARD_SIZE,
}
self.diacount = {
Marubatsu.CIRCLE: [0] * 2,
Marubatsu.CROSS: [0] * 2,
}
修正箇所
from marubatsu import Marubatsu, List1dBoard
class ArrayBoard(List1dBoard):
def __init__(self, board_size=3, count_linemark=False):
self.BOARD_SIZE = board_size
self.count_linemark = count_linemark
- self.board = [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2)
+ self.board = array("w", [Marubatsu.EMPTY] * (self.BOARD_SIZE ** 2))
元と同じなので省略
上記の定義後に、下記のプログラムで ArrayBoard を利用する場合の ベンチマークを実行 することで ArrayBoard クラスの 処理速度を計測 することにします。実行結果の 対戦成績 が以前の記事で行ったベンチマークと 同じである5ことから、ArrayBoard を利用した場合に 正しい処理を行うことができることが確認 できます。
from util import benchmark
boardclass = ArrayBoard
for count_linemark in [False, True]:
print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
benchmark(mbparams={"boardclass": boardclass, "count_linemark": count_linemark})
print()
実行結果
boardclass: ArrayBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 14702.96it/s]
count win lose draw
o 29454 14352 6194
x 14208 29592 6200
total 43662 43944 12394
ratio win lose draw
o 58.9% 28.7% 12.4%
x 28.4% 59.2% 12.4%
total 43.7% 43.9% 12.4%
ai14s VS ai2
100%|██████████| 50000/50000 [00:48<00:00, 1023.88it/s]
count win lose draw
o 49446 0 554
x 44043 0 5957
total 93489 0 6511
ratio win lose draw
o 98.9% 0.0% 1.1%
x 88.1% 0.0% 11.9%
total 93.5% 0.0% 6.5%
ai_abs_dls
18.8 ms ± 1.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
boardclass: ArrayBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:03<00:00, 14645.31it/s]
count win lose draw
o 29454 14352 6194
x 14208 29592 6200
total 43662 43944 12394
ratio win lose draw
o 58.9% 28.7% 12.4%
x 28.4% 59.2% 12.4%
total 43.7% 43.9% 12.4%
ai14s VS ai2
100%|██████████| 50000/50000 [00:26<00:00, 1903.46it/s]
count win lose draw
o 49446 0 554
x 44043 0 5957
total 93489 0 6511
ratio win lose draw
o 98.9% 0.0% 1.1%
x 88.1% 0.0% 11.9%
total 93.5% 0.0% 6.5%
ai_abs_dls
19.8 ms ± 1.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
下記は前回の記事の最後で行った ListBoard と List1dBoard クラスのベンチマークの結果に上記の ArrayBoard の実行結果を加えた表 です。
boardclass | count_linemark |
ai2 VS ai2
|
ai14s VS ai2
|
ai_abs_dls |
---|---|---|---|---|
ListBoard | False |
14916.51 回/秒 | 1116.53 回/秒 | 17.4 ms |
ListBoard | True |
15463.39 回/秒 | 2030.27 回/秒 | 17.4 ms |
List1dBoard | False |
17404.27 回/秒 | 1145.23 回/秒 | 16.7 ms |
List1dBoard | True |
17176.56 回/秒 | 2152.38 回/秒 | 17.3 ms |
ArrayBoard | False |
14702.96 回/秒 | 1023.88 回/秒 | 18.8 ms |
ArrayBoard | True |
14645.31 回/秒 | 1903.46 回/秒 | 19.8 ms |
上記の表から残念ながら ArrayBoard を利用したベンチマークの結果は、List1dBoard を利用した場合よりも 処理速度が遅くなる ことがわかります。これは、Python の array に対する要素の参照と代入処理 の処理速度が list よりも遅い からです。
ArrayBoard の処理速度は List1dBoard よりも遅い ので今後の記事で ArrayBoard を利用する予定はありません が、ポリモーフィズムによって ゲーム盤を array で表現するクラスを簡単に定義できる ことを示したかったので、ArrayBoard の実装を紹介しました。
list と array の違いの検証と使い分け
Python の list と array の違い を検証することで、list と array を どのように使い分ければ良いか について説明します。
要素の参照と代入処理の比較
下記のプログラムは 整数の要素を 100 万個 持つ list と array に対して 0 番の要素の参照と代入 を行う処理の処理時間を %timeit で計測するプログラムです。
-
1 行目:100 万個の整数の要素を持つ list を
l
に代入する -
2 行目:
l
をarray
の 2 番目の実引数に記述することで、100 万個の整数の要素を持つ array をa
に代入する -
3 ~ 12 行目:
l
とa
の 0 番の要素に対する参照と代入処理の処理時間を %timeit を利用して計測する
1 l = [i for i in range(1000000)]
2 a = array("i", l)
3 print("参照")
4 print("list")
5 %timeit l[0]
6 print("array")
7 %timeit a[0]
8 print("代入")
9 print("list")
10 %timeit l[0] = 1
11 print("array")
12 %timeit a[0] = 1
行番号のないプログラム
l = [i for i in range(1000000)]
a = array("i", l)
print("参照")
print("list")
%timeit l[0]
print("array")
%timeit a[0]
print("代入")
print("list")
%timeit l[0] = 1
print("array")
%timeit a[0] = 1
実行結果
参照
list
22.6 ns ± 2.29 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
array
35.2 ns ± 1.47 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
代入
list
22.9 ns ± 1.4 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
array
54.7 ns ± 0.967 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
実行結果から、参照と代入処理 は いずれも array のほうが遅い ことが確認できます。これが、ArrayBoard のほうが List1dBoard よりも 処理速度が遅い原因 です。
データの浅いコピーと深いコピー処理の比較
次に、下記のプログラムで上記で作成した 100 万個の整数の要素を持つ list と array に対して、データの 浅いコピーと深いコピー を行う処理の処理速度を計測します。
from copy import copy, deepcopy
print("copy")
print("list")
%timeit copy(l)
print("array")
%timeit copy(a)
print("deepcopy")
print("list")
%timeit deepcopy(l)
print("array")
%timeit deepcopy(a)
実行結果
copy
list
9.47 ms ± 164 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
array
1.06 ms ± 22.1 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
deepcopy
list
277 ms ± 12.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
array
1.09 ms ± 15.6 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
実行結果から以下の事が確認できました。
- 浅いコピー は array のほうが list よりも 約 10 倍速い
- 深いコピー は array のほうが list よりも 約 250 倍速い
array のほうが浅いコピーの処理速度が速い理由は Python の array と list のデータが具体的にどのように表現されているかがわからないので何とも言えません。理由をご存じの方がいればコメントなどに書いていただければ助かります。
array のほうが 深いコピーの処理速度が速い理由 は、array では 要素のデータが連続したメモリに記録 されているため、それらのデータを まとめてコピー することができるからです。一方、list では それぞれの要素 のデータが 異なるオブジェクト内に記録 されており、要素のデータが メモリの中の様々な場所に散らばっている ため、それぞれの要素に記録された オブジェクトのデータを別々にコピーする必要が生じるため 処理速度が低下します。
array と list の違いを 100 巻の漫画の単行本で例えると、array は 100 巻の単行本だけが順番に並べられた本棚に相当します。一方 list は様々な本が収められた書庫に 100 巻の単行本があちこちに分散してしまわれている状況で、100 巻の単行本が書庫のどこにしまわれているかを表す目録を記録することに相当します。
また、この後で説明するように、array のほうが 全体のデータの量が少ない ことも、深いコピーの 処理時間が短くなる原因 の一つです。
このように、array は コピーの処理 を行う場合は list よりも 処理速度が速く なります。ただし、AI どうしの対戦 で 〇× ゲームのプログラムでゲーム盤のデータを コピーする処理は行われない ので、AI どうしの対戦の処理速度には 影響を及ぼしません6。
データサイズの比較
オブジェクトの データサイズ は組み込みモジュールである sys モジュールの getsizeof
という関数で計算することができます。例えば 1
という 数値型のデータ は下記のプログラムから 28 バイト であることが確認できます。
from sys import getsizeof
print(getsizeof(1))
実行結果
28
なお、下記の getsizeof
のドキュメント内に「オブジェクトに直接起因するメモリ消費のみを表し、参照するオブジェクトは含みません」と記述されていることから、list など の 内部に他のオブジェクトが記録 されているオブジェクトの 全体のデータサイズ を計算するためには それらのオブジェクトのデータサイズを加える 必要があります。
getsizeof
は int などの組み込みデータ型のデータサイズは正確に計算できますが、組み込みデータ型以外のデータサイズを計算できない場合があります。
下記は先ほどの 100 万個の整数の要素を持つ list と array の データサイズを計算して表示 するプログラムです。list の場合は list のオブジェクト と、その中の すべての要素のデータサイズ の 合計を計算 しています。array はその中に 他のオブジェクトを記録していない ので getsizeof
で直接データサイズを計算 できます。
print("list")
size = sys.getsizeof(l)
for data in l:
size += sys.getsizeof(data)
print(size)
print("array")
print(sys.getsizeof(a))
実行結果
list
36448728
array
4000080
上記の実行結果から 1 つの要素あたりのデータサイズ が array では 約 4 バイト であるのに対し list ではその 約 10 倍 の 約 36 バイト であることがわかります。このような差が生じるのは、list では 1 つ 1 つの 要素のデータ をそのデータを管理する オブジェクトを作成して記録 するため、データの管理に必要なデータ の分だけ データサイズが増える からです。
データの 深いコピー を行う際に list と array で 処理時間が異なる理由の一つ は このデータサイズの違い です。
下記は list と array の性質の違い をまとめた表です。性質が良いほうを太字 にしました。
list | array | |
---|---|---|
要素のデータ型 | 任意 | 1 種類のみ |
扱える次元(添字の数) | 任意の次元 | 1 次元のみ |
参照と代入 | 速い | 遅い |
浅いコピー | 遅い | 速い |
深いコピー | 非常に遅い | 速い |
データサイズ | 大きい | 小さい |
list そのものは 1 次元のデータしか扱えませんが、list の要素に他の list を入れ子で記録することで 2 次元以上の list を扱うことができます。一方、array はその要素に他の array を記録することはできないので 1 次元のデータしか扱えません。
list や array の要素の数や、記録するデータ型によって若干変わるかもしれませんが、おおむね上記の表のような性質がある と思います。list と array の使い分 けは 上記の表の性質を考慮して行うと良い でしょう。なお、コピーの処理速度やデータサイズが気にならない ようなプログラムでは、データ型の制限がない list を利用すれば良い と思います。
numpy を利用したゲーム盤のデータ構造
上記で紹介した array は、list と比較して メモリの消費量が少ない、コピーの処理が速い という 利点 がありますが、参照と代入処理 に関しては list よりも 遅く、2 次元以上の array を扱うことができない という欠点があります。
Python で 配列を利用 するプログラムを記述する場合は、array ではなく numpy という 配列を扱うことができるモジュールを利用するのが一般的 だと思います。numpy は 高機能 で 特に大量の配列の要素の計算処理 を行う場合は list よりも 処理速度が非常に速い ため頻繁に利用されています。プログラム言語として Python を採用する理由の一つ が numpy にあると言っても過言ではない と思います。
numpy は組み込みモジュールではないので インストールする必要 がありますが、matplotlib などの 多くのモジュールで numpy が利用されている ので、numpy を利用するモジュールをインストール すると 自動的に numpy もインストール されます。そのため本記事のように matplotlib がインストール済 であれば numpy をインストールする必要はありません。
numpy は下記のプログラムのように 一般的に np という名前でインポートして利用 します。下記のプログラムを実行して エラーが発生しなければ numpy はインストール済 です。エラーが発生する場合は 他のモジュールと同様の方法で numpy を仮想環境にインストールして下さい。
import numpy as np
モジュールのバージョン は一般的に __version__
という特殊属性に代入 されているので、numpy のバージョン は下記のプログラムで確認できます。実行結果のように、筆者の仮想環境にインストールされている numpy のバージョンは 2.3.2 でした。なお、多少バージョンが異なっていても本記事のプログラムは正しく動作する と思います。
print(np.__version__)
実行結果
2.3.2
なお、下記の numpy の本家のウェブページによると、本記事を執筆した 2025/10/04 の時点での numpy の最新バージョンは 2.3 のようです。
下記の日本語版の numpy のウェブページでは、最新のバージョンが 1.26 と表記されているようですが、このウェブぺージの更新日時が 2024年8月18日となっており、1 年以上も更新されていないようです。このように、海外で作成 されたモジュールの 日本語版のウェブページ は 更新がされていない場合が良くある ので、最新の情報が必要な場合は英語のページを見たほうが良い と思います。
numpy の特徴
numpy が扱う配列 のデータには、下記のような特徴があります。他にも便利な特徴がありますが、numpy の便利な機能をすべて紹介すると記事がいくつあっても足りないと思いますので、本記事では主に〇×ゲームのプログラムで利用する機能を紹介する予定です。
- list や array と同様に 0 以上の整数の添字 で要素の 参照と代入 を行う
- 添字 に対して array や list よりも 柔軟な記述 を行うことができる
- array と異なり、2 次元以上の配列 を扱うことができる
- array と同様に基本的には 同じデータ型のデータ しか扱えない
- list や array と異なり、要素の数を後から変更することができない7
- 配列のデータに対する 様々な便利な数学的な計算を行うための関数が定義 されている
- 大量の要素に対する計算 の処理速度が list よりも 高速である
- ブロードキャスト とよばれる仕組みで 便利な計算 を行うことができる
上記のうちのいくつかについては今後の記事で紹介する予定です。
numpy の配列(ndarray)の作成方法
numpy の配列 は numpy モジュールの ndarray という名前の クラスで定義 されています。ndarray は n 次元(dimensional)配列(array)の略 で、n は任意の次元 の配列のデータを扱うことができることを意味します。本記事では numpy の配列 を以後は ndarray と表記 することにします。
ndarray は 様々な方法で作成 できますが、今回の記事では先程紹介した array と同様に list などの 反復可能オブジェクトから ndarray を作成する方法 を紹介します。
具体的には下記のプログラムのように numpy モジュールで定義された array
クラスの実引数 に 反復可能オブジェクトを記述 することで ndarray を作成 することができます。array と異なり listb
のような 2 次元の list からも ndarray を作成 ことができます。
なお、numpy モジュールの関数やクラス は下記のプログラムのように np.関数名
のように記述 して利用するのが一般的です。
lista = [1, 2, 3]
listb = [[1, 2], [3, 4], [5, 6]]
arraya = np.array(lista)
arrayb = np.array(listb)
print(lista)
print(arraya)
print(listb)
print(arrayb)
実行結果
[1, 2, 3]
[1 2 3]
[[1, 2], [3, 4], [5, 6]]
[[1 2]
[3 4]
[5 6]]
実行結果からわかるように ndarray のデータを print
で表示 した場合は、list の場合とは異なり 要素が ,
で区切られません。また、2 次元以上の ndarray の場合は list と異なり 複数行に分けて表示 されますが、それ以外の点では list の場合と同じ表示内容 なので慣れればそれほど違和感は生じないと思います。
要素のデータ型、次元、形状の表示
ndarray の 要素のデータ型 は、実引数に記述した 反復可能オブジェクト のデータから 自動的に判定されて設定 され、dtype
という属性に代入 されます。下記のプログラムから、上記の arraya
のデータ型 は int64
という整数型のデータ であることが確認できます。
print(arraya.dtype)
実行結果
int64
上記の int64
は 64 ビット = 8 バイト のデータで 整数を表現するデータ型 で、$-2^{63}$ ~ $2^{63} - 1$ の範囲の整数を表現することができます。
他にも 32 ビットで $-2^{31}$ ~ $2^{31} - 1$ の範囲の整数を表現できる int32
、16 ビットや 8 ビットで整数を表現できる int16
や int8
などのさまざまなデータ型があります。
なお、int32
は表現できる整数の範囲が int64
よりも少なくなりますが、データサイズが半分になるという利点があります。int16
や int8
も同様です。
ndarray が扱うデータ型は、ndarray の作成時にキーワード引数 dtype
を記述することで指定することができます。numpy のデータ型の詳細については下記のリンク先を参照して下さい。
ndarray の配列の 次元は ndims
という属性に、それぞれの次元のインデックスの種類の数 を表す 配列の形状(shape) は shape
という属性に代入されます。
print(arraya.ndim)
print(arraya.shape)
print(arrayb.ndim)
print(arrayb.shape)
実行結果
1
(3,)
2
(3, 2)
配列の形状 を表す shape
属性は tuple で表現 され、先頭の要素から順番 に 各次元のインデックスの種類の数 を表します。インデックスは 0 以上の整数 で表現されるので、各次元のインデックスの範囲 は 0 ~ インデックスの種類の数 - 1 となります。
以前の記事 で説明したように、要素が 1 つの tuple は上記の実行結果の (3, )
のように、括弧の中の 0 番の要素の後に ,
が表記されます
上記のプログラムの実行結果から arraya
と arrayb
の次元とインデックスの範囲は下記の表のようになることがわかります。
次元 | 1 つ目のインデックスの範囲 | 2 つ目のインデックスの範囲 | |
---|---|---|---|
arraya |
1 | 0 ~ 2 | |
arrayb |
2 | 0 ~ 2 | 0 ~ 1 |
ndarray の要素 は下記のプログラムのように list と同様 に 次元の数だけ添字を記述 して 値の参照と代入 を行うことができます。
lista[2] = 5
arraya[2] = 5
print(lista[2])
print(lista)
print(arraya[2])
print(arraya)
listb[0][1] = 5
arrayb[0][1] = 5
print(listb[0][1])
print(listb)
print(arrayb[0][1])
print(arrayb)
実行結果
5
[1, 2, 5]
5
[1 2 5]
5
[[1, 5], [3, 4], [5, 6]]
5
[[1 5]
[3 4]
[5 6]]
NpBoard クラスの定義
2 次元の list から作成された 2 次元の ndarray は 2 次元の list と同様の方法 で 要素の参照と代入 を行うことができます。従って、2 次元の ndarray でゲーム盤を表す NpBoard クラス は、ArrayBoard クラスと同様の方法 で下記のプログラムのように定義できます。
- 3 行目:2 次元の ndarray でゲーム盤のデータを表現する以外は ListBoard クラスと同じ処理を行うので、ListBoard クラスを継承して NpBoard クラスを定義する
- 7 行目:2 次元の list でゲーム盤を表現する場合と同じ要素を持つ、文字列型のデータを扱う numpy の配列を board 属性に代入するように修正した
1 from marubatsu import ListBoard
2
3 class NpBoard(ListBoard):
4 def __init__(self, board_size=3, count_linemark=False):
5 self.BOARD_SIZE = board_size
6 self.count_linemark = count_linemark
7 self.board = np.array([[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)])
元と同じなので省略
行番号のないプログラム
from marubatsu import ListBoard
class NpBoard(ListBoard):
def __init__(self, board_size=3, count_linemark=False):
self.BOARD_SIZE = board_size
self.count_linemark = count_linemark
self.board = np.array([[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)])
if self.count_linemark:
self.rowcount = {
Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
Marubatsu.CROSS: [0] * self.BOARD_SIZE,
}
self.colcount = {
Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
Marubatsu.CROSS: [0] * self.BOARD_SIZE,
}
self.diacount = {
Marubatsu.CIRCLE: [0] * 2,
Marubatsu.CROSS: [0] * 2,
}
修正箇所
from marubatsu import ListBoard
class NpBoard(ListBoard):
def __init__(self, board_size=3, count_linemark=False):
self.BOARD_SIZE = board_size
self.count_linemark = count_linemark
- self.board = [[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)]
+ self.board = np.array([[Marubatsu.EMPTY] * self.BOARD_SIZE for y in range(self.BOARD_SIZE)])
if self.count_linemark:
self.rowcount = {
Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
Marubatsu.CROSS: [0] * self.BOARD_SIZE,
}
self.colcount = {
Marubatsu.CIRCLE: [0] * self.BOARD_SIZE,
Marubatsu.CROSS: [0] * self.BOARD_SIZE,
}
self.diacount = {
Marubatsu.CIRCLE: [0] * 2,
Marubatsu.CROSS: [0] * 2,
}
上記の定義後に下記のプログラムで NpBoard クラスのインスタンスを作成 し下記の表示を行います。
- ゲーム盤を表す 2 次元の ndarray が代入された
board
属性 -
board
属性のdtype
、ndim
、shape
属性
npb = NpBoard()
print(npb.board)
print(npb.board.dtype)
print(npb.board.ndim)
print(npb.board.shape)
実行結果
[['.' '.' '.']
['.' '.' '.']
['.' '.' '.']]
<U1
2
(3, 3)
上記の実行結果から下記のことが確認できました。
- 空のマスが配置された 3 × 3 の 2 次元のゲーム盤を表す ndarray が代入されている
- ndarray のデータ型として
<U1
が設定されている。<U1
は、ユニコード(unicode)という文字コードの 1 文字分の文字列型のデータ を表す - ndarray の次元(ndim)は 2 である
- ndarray の形状は 3 × 3 のゲーム盤の形状 を表す
(3, 3)
である
上記の定義後に、下記のプログラムで NpBoard を利用した場合の ベンチマークを実行 することで NpBoard クラスの処理速度を計測 することにします。実行結果の 対戦成績 が 以前の記事で行った ベンチマークと同じである ことから、NpBoard を利用した場合に 正しい処理を行うことができることが確認 できます。
boardclass = ArrayBoard
for count_linemark in [False, True]:
print(f"boardclass: {boardclass.__name__}, count_linemark {count_linemark}")
benchmark(mbparams={"boardclass": boardclass, "count_linemark": count_linemark})
print()
実行結果
boardclass: NpBoard, count_linemark False
ai2 VS ai2
100%|██████████| 50000/50000 [00:08<00:00, 5924.49it/s]
count win lose draw
o 29454 14352 6194
x 14208 29592 6200
total 43662 43944 12394
ratio win lose draw
o 58.9% 28.7% 12.4%
x 28.4% 59.2% 12.4%
total 43.7% 43.9% 12.4%
ai14s VS ai2
100%|██████████| 50000/50000 [01:18<00:00, 633.71it/s]
count win lose draw
o 49446 0 554
x 44043 0 5957
total 93489 0 6511
ratio win lose draw
o 98.9% 0.0% 1.1%
x 88.1% 0.0% 11.9%
total 93.5% 0.0% 6.5%
ai_abs_dls
42.9 ms ± 1.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
boardclass: NpBoard, count_linemark True
ai2 VS ai2
100%|██████████| 50000/50000 [00:07<00:00, 6832.91it/s]
count win lose draw
o 29454 14352 6194
x 14208 29592 6200
total 43662 43944 12394
ratio win lose draw
o 58.9% 28.7% 12.4%
x 28.4% 59.2% 12.4%
total 43.7% 43.9% 12.4%
ai14s VS ai2
100%|██████████| 50000/50000 [00:33<00:00, 1475.99it/s]
count win lose draw
o 49446 0 554
x 44043 0 5957
total 93489 0 6511
ratio win lose draw
o 98.9% 0.0% 1.1%
x 88.1% 0.0% 11.9%
total 93.5% 0.0% 6.5%
ai_abs_dls
43.1 ms ± 1.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
下記は先ほどの ArrayBoard のベンチマークの結果の表に 上記の実行結果を加えた表 です。
boardclass | count_linemark |
ai2 VS ai2
|
ai14s VS ai2
|
ai_abs_dls |
---|---|---|---|---|
ListBoard | False |
14916.51 回/秒 | 1116.53 回/秒 | 17.4 ms |
ListBoard | True |
15463.39 回/秒 | 2030.27 回/秒 | 17.4 ms |
List1dBoard | False |
17404.27 回/秒 | 1145.23 回/秒 | 16.7 ms |
List1dBoard | True |
17176.56 回/秒 | 2152.38 回/秒 | 17.3 ms |
ArrayBoard | False |
14702.96 回/秒 | 1023.88 回/秒 | 18.8 ms |
ArrayBoard | True |
14645.31 回/秒 | 1903.46 回/秒 | 19.8 ms |
NpBoard | False |
5924.49 回/秒 | 633.71 回/秒 | 42.9 ms |
NpBoard | True |
6832.91 回/秒 | 1475.99 回/秒 | 43.1 ms |
残念ながら NpBoard クラスを利用した場合の処理速度が ArrayBoard クラスを利用した場合よりも 大幅に遅くなる ことが確認できます。
これは、ndarray の要素の参照と代入処理の処理速度 が、list や array よりもさらに遅い ことが原因です。そのことは、下記のプログラムの実行結果から確認できます。
n = np.array(l)
print("参照")
print("list")
%timeit l[0]
print("array")
%timeit a[0]
print("numpy")
%timeit n[0]
print("代入")
print("list")
%timeit l[0] = 1
print("array")
%timeit a[0] = 1
print("numpy")
%timeit n[0] = 1
実行結果
参照
list
23.1 ns ± 1.49 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
array
36.2 ns ± 1.5 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
numpy
72.3 ns ± 2.29 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
代入
list
22.5 ns ± 0.176 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
array
57.9 ns ± 2.81 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
numpy
69.5 ns ± 1.49 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
このように、ndarray は 個別の要素 の 参照と代入処理 を 頻繁に行う場合 は残念ながら list よりも 処理速度が遅くなります。一方で、個別の要素ではなく 全体の要素の合計など、複数の要素をまとめて計算 するような処理の場合は、ndarray のほうが list よりも 高速に処理を行う ことができます。また、numpy には 配列に対して良く行われる処理 を 高速に行うことができる関数が大量に用意 されているという利点があります。
次回の記事では、numpy の関数 を利用することで、NpBoard クラスの高速化 と、プログラムを簡潔にわかりやすく記述 する方法について紹介することにします。
今回の記事のまとめ
今回の記事では array と numpy の ndarray を利用して ゲーム盤のデータ構造を表現するクラスを定義 しました。残念ながらこれらを利用した場合の 処理速度 は ListBoard クラスや List1dBoard クラスよりも 遅いことが判明 しました。ただし、ndarray を利用したクラスの処理は改善の余地があるので、次回の記事ではその 改善を行う ことにします。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
marubatsu.py | 本記事で更新した marubatsu_new.py |
次回の記事
近日公開予定です
-
Wikipedia では抽象データ型と表記されているようです ↩
-
要素の数(長さ)が決まっておらず、後から要素の追加、削除を行うことができる配列のこと ↩
-
Python の array は頭文字が小文字ですが、JavaScript の Array は頭文字を大文字で表記します ↩
-
C 言語など、2 次元の配列を扱うことができるプログラム言語もあります ↩
-
前回の記事の最後のベンチマークと対戦成績が若干異なることに気が付きましたが、これは前回の記事の最後のベンチマークでは乱数の初期化を行っていないことが原因です。乱数の初期化を行った場合の処理時間は大きく変わらないので前回の記事のベンチマークはそのままにします ↩
-
unmove
メソッドを導入する前はdeepcopy
でゲーム盤のデータの深いコピーを行っていたので、その時は array でゲーム盤を導入することによって処理速度が速くなったかもしれません ↩ -
このような配列の事を静的な配列と呼びます。なお、要素の追加・削除を行う numpy の関数は存在しますが、それらの関数は array や list のように元のデータに対して要素を追加するのではなく、要素が追加された新しいデータを作成します ↩