目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
これまでに作成した AI
これまでに作成した AI の アルゴリズム は以下の通りです。
関数名 | アルゴリズム |
---|---|
ai1 |
左上から順 に 空いているマス を探し、最初に見つかったマス に 着手 する |
ai2 ai2s
|
ランダム なマスに 着手 する |
ai3 ai3s
|
真ん中 のマスに 優先的 に 着手 する 既に 埋まっていた場合 は ランダム なマスに 着手 する |
ai4 |
真ん中、隅 のマスの 順 で 優先的 に 着手 する 既に 埋まっていた場合 は ランダム なマスに 着手 する |
ai5 |
勝てる場合 に 勝つ そうでない場合は ランダム なマスに 着手 する |
ai6 |
勝てる場合 に 勝つ そうでない場合は 相手の勝利 を 阻止 する そうでない場合は ランダム なマスに 着手 する |
ai7 |
真ん中 のマスに 優先的 に 着手 する そうでない場合は 勝てる場合 に 勝つ そうでない場合は 相手の勝利 を 阻止 する そうでない場合は ランダム なマスに 着手 する |
基準となる ai2
との 対戦結果(単位は %)は以下の通りです。太字 は ai2 VS ai2
よりも 成績が良い 数値を表します。欠陥 の列は、アルゴリズム に 欠陥 があるため、ai2
との 対戦成績 が 良くても強い とは 限らない ことを表します。欠陥の詳細については、関数名のリンク先の説明を見て下さい。
関数名 | o 勝 | o 負 | o 分 | x 勝 | x 負 | x 分 | 勝 | 負 | 分 | 欠陥 |
---|---|---|---|---|---|---|---|---|---|---|
ai1 |
78.1 | 17.5 | 4.4 | 44.7 | 51.6 | 3.8 | 61.4 | 34.5 | 4.1 | あり |
ai2 |
58.7 | 28.8 | 12.6 | 29.1 | 58.6 | 12.3 | 43.9 | 43.7 | 12.5 | |
ai3 |
69.3 | 19.2 | 11.5 | 38.9 | 47.6 | 13.5 | 54.1 | 33.4 | 12.5 | |
ai4 |
83.0 | 9.5 | 7.4 | 57.2 | 33.0 | 9.7 | 70.1 | 21.3 | 8.6 | あり |
ai5 |
81.2 | 12.3 | 6.5 | 51.8 | 39.8 | 8.4 | 66.5 | 26.0 | 7.4 | |
ai6 |
88.9 | 2.2 | 8.9 | 70.3 | 6.2 | 23.5 | 79.6 | 4.2 | 16.2 | |
ai7 |
95.8 | 0.2 | 4.0 | 82.3 | 2.4 | 15.3 | 89.0 | 1.3 | 9.7 |
デバッグ表示の実装
評価値を利用したアルゴリズムに限った話ではありませんが、実装 した プログラム が 意図通り に 動作しない ことは 良くある事 です。ある程度以上の規模の プログラム を、バグ を一度も 発生させず に 完成する のは、ほぼ不可能 であると言っても良いでしょう。そのような場合に、バグの修正 に 役立つ のが、処理の経過 を 表示 する デバッグ表示 です。
そこで、今後のことを考えて、評価値を利用したアルゴリズムのひな形となる ai_by_score
を、デバッグ表示 の 機能 を持つように、プログラムを 改良 することにします。
デバッグ表示 の機能の 実装 は、以前の記事 で実装した test_judge
と 同じ方法 で、下記のプログラムのように行うことができます。
-
仮引数
debug
によって デバッグ表示 を行うかどうかを 選択できる ようにする -
if 文 を使って、
debug
の値がTrue
の場合 に、デバッグ表示 を行う
def test_judge(testcases, debug=False):
略
if debug:
print(mb)
略
デバッグ表示を行う関数の定義
上記のプログラムでは、if debug:
によって デバッグ表示 を行うかどうかを 判定 する 行 と、print
を使って デバッグ表示 を行う 行 を 記述 していますが、毎回 if debug:
を記述するのは 面倒 です。そこで、この 2 行の処理 を行う 関数を定義 することにします。
その 関数 の 処理、名前、入力、出力 は、以下のように決めることにします。
処理:実引数 に記述した 値によって、デバッグ表示 を行うかどうかを 判断 して 表示 する
関数の名前:デバッグ(debug)の 表示(print)を行うので、debug の 頭文字 をとって dprint
1 という 名前 にする
入力:デバッグ表示 を 行うか どうかを 表す値 を 仮引数 debug
に、print
で デバッグ表示 を行う データ を 仮引数 data
に 代入 する
出力:画面への表示 という形での 出力を行う が、返り値 という形の 出力は行わない
具体的には、下記のような関数を定義します。
-
1 行目:デバッグ表示 を 行うか どうかを 表すデータ を 仮引数
debug
に、デバッグ表示 で 表示するデータ を 仮引数data
に 代入 するようにdprint
を 定義 する -
2、3 行目:
debug
がTrue
の場合 に、print
でdata
を表示 する
def dprint(debug, data):
if debug:
print(data)
下記のプログラムは、dprint
の 利用例 です。1 行目 では、dprint
の 1 つ目 の 実引数 に True
を記述 しているので、2 つ目 の 実引数の値 である debug = True が 表示 されますが、2 行目 では、1 つ目 の 実引数 に False
を記述 しているので 何も表示されません。
dprint(True, "debug = True")
dprint(False, "debug = False")
実行結果
debug = True
定義した関数の問題点
先程定義した dprint
にはいくつかの 問題点 があります。その一つは、下記のプログラムのように、複数のデータ を dprint
で 表示 しようとした場合に エラーが発生 する点です。
dprint(True, 1, 2)
実行結果
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[3], line 1
----> 1 dprint(True, 1, 2)
TypeError: dprint() takes 2 positional arguments but 3 were given
上記のエラーメッセージは、以下のような意味を持ちます。
- TypeError
データ型(Type)に関するエラー
- dprint() takes 2 positional arguments but 3 were given
dprint は 2 つの位置引数(positional arguments)を受け取る(takes)が、3 つの位置引数が与えられている(were given)
エラーの 原因 は、dprint
の 仮引数 が debug
と data
の 2 つしかない にもかかわらず、True
、1
、2
の 3 つ の 実引数 を 記述 して dprint
を 呼び出した 点にあります。
この 問題 は、dprint
の中 で利用している組み込み関数 print
が、任意の数 の 実引数 を 記述できる のに対して、dprint
には 2 つの 実引数 を 記述する必要 がある点にあります。
この 問題を解決 するためには、dprint
に対して、任意の数 の 実引数 を 記述 して呼び出すことが できるようにする 必要がありますが、これまで に本記事で紹介してきた 方法 では、任意の数 の 実引数 を記述するような 関数を定義 する事は できません。そこで、今回の記事では、任意の数 の 実引数 を 記述できる ような 関数を定義 する 方法 を紹介します。
可変長引数と実引数の展開
任意の数 の 実引数 を 記述 して呼び出すことができる 関数 の 仮引数 のことを、可変長引数 と呼びます。可変長引数 には、位置引数 に 対する ものと、キーワード引数 に 対する ものの 2 種類 があります。
位置引数に対する可変長引数
まず、位置引数 に対する 可変長引数 について説明します。位置引数 に対する 可変長引数 は、下記のプログラムのように、仮引数の名前 の 先頭 に 半角の *
を 記述 します。
def a(*args):
print(args)
Python では、位置引数 に対する 可変長引数 の 名前 を args
(実引数 を表す argument の 複数形 の略)にする 慣習2 があるので、上記のプログラムでも 名前 を args
としました。
なお、初心者が 勘違いしやすい点 として、先頭 の *
を 含んだ *args
が 名前 であるというものがあります。先頭 の *
は、その 仮引数 が 位置引数 に対する 可変長引数 であることを 示すための記号 なので、上記の 可変長引数 の 名前 は args
である点に 注意 して下さい。
なお、「位置引数に対する可変長引数」という表記は長いので、以後は、位置引数に対する可変長引数 として 一般的に記述 される「*args
」と記述することにします。
仮引数 に *args
が 記述 されている 関数 に対して、任意の数 の 位置引数 を 記述 して呼び出すと、下記のプログラムのように、記述 した 位置引数 の 値 を、記述した順番 で 要素 とする tuple が、args
に代入3 されます。1 行目 や 3 行目 のように、実引数の数 が 1 や 0 の 場合 でも、必ず tuple が 代入 される点に 注意 して下さい。
a(1) # 実引数の数が 1 つしかない場合は、1 ではなく、要素を 1 つ持つ tuple が代入される
a(1, 2, 3)
a() # 実引数が記述されていない場合は、None ではなく、空の tuple が代入される
実行結果
(1,)
(1, 2, 3)
()
引数に関する用語と分類の整理
この後の説明を行う前に、引数 に関する一部の 用語 は、仮引数 を表す用語であるか、実引数 を表す用語であるかが 分かりづらい ので、表 にまとめて 整理 することにします。
なお、通常の仮引数 という 用語 はおそらく 存在しない と思いますが、本記事では、デフォルト引数、可変長引数 以外 の 仮引数 を、区別 して「通常の仮引数」と 表記 することにします。まだ 説明していない 表の 項目 については、後で詳しく説明 します。
用語 | 分類 | 記述例 |
---|---|---|
位置引数 | 実引数 | a(1) |
キーワード引数 | 実引数 | a(x=1) |
反復可能オブジェクトの展開 | 実引数 | a(*x) |
マッピング型の展開 | 実引数 | a(**x) |
通常の仮引数 | 仮引数 | def a(x): |
デフォルト引数 | 仮引数 | def a(x=1): |
位置引数に対する可変長引数 | 仮引数 | def a(*args): |
キーワード引数に対する可変長引数 | 仮引数 | def a(**kwargs): |
関数の仮引数と、実引数に関する詳細は、下記のリンク先を参照して下さい。
*args
と他の仮引数の併用
*args
は、通常の仮引数 や デフォルト引数 と 併用 することができます。その場合は、*args
は、すべて の 通常の仮引数 より 後 に 記述 する 必要 があります。なお、*args
と デフォルト引数 の記述の順番については、後で説明 します。
*args
と、通常の仮引数 や デフォルト引数 を 併用 した場合は、位置引数 として 記述 された 実引数 は、以下の仮引数に代入されます。
- 対応 する 通常の仮引数 や デフォルト引数 が 存在すれば、その仮引数 に代入される
- それらが 存在しなければ、位置引数 の 値 を 記述 された 順番 で 要素 とする tuple が、
args
に代入 される
別の言葉で説明すると、「余った位置引数 の 値 を 要素 として持つ tuple が args
に代入 される」という処理が行われます。
従って、dprint
を、下記のプログラムのように修正することで、2 つ目以降 の 実引数 に、任意の数 の 位置引数を記述 することができるようになります。なお、元 の dprint
では、2 つ目 の 仮引数 の 名前 を data
としていましたが、Python の 慣習 に従って、2 つ目 の位置引数に対応する可変長引数の 名前 を args
に 修正 しました。
def dprint(debug, *args):
if debug:
print(args)
修正箇所
-def dprint(debug, data):
+def dprint(debug, *args):
if debug:
- print(data)
+ print(args)
下記のプログラムを実行すると、1 つ目 の 実引数 の値が、dprint
の 仮引数 debug
に 代入 され、2 つ目以降 の 位置引数 の 値 が、tuple に変換 されて args
に 代入 されます。
従って、1 ~ 3 行目 の 実行結果 は、先程のプログラム の実行結果と 同じ になります。また、4 行目 を 実行 しても何も 表示 は 行われません。
dprint(True, 1)
dprint(True, 1, 2, 3)
dprint(True)
dprint(False, 1, 2, 3)
実行結果
(1,)
(1, 2, 3)
()
反復可能オブジェクトの展開
先程の dprint
には 問題 があります。dprint
は、1 つ目 の 実引数 が True
の場合に、2 つ目以降 の 実引数 を使って print
で 表示を行う 関数として 定義したつもり ですが、実際 には、下記のプログラムのように、dprint
で 表示 される内容と、print
で 表示 される内容は 異なります。なお、6 行目 では、空行(何も表示されない行)が 表示 されています。
dprint(True, 1)
print(1)
dprint(True, 1, 2, 3)
print(1, 2, 3)
dprint(True)
print()
実行結果
(1,)
1
(1, 2, 3)
1 2 3
()
この問題は、反復可能オブジェクト の 各要素 を、順番 に 位置引数 として 記述 したと みなして、関数呼び出しを行う という、反復可能オブジェクトの展開 という 記述 を行うことで 解決 できます。反復可能オブジェクトの展開 は、実引数 を 記述 する際に、反復可能オブジェクト の 先頭 に 半角の *
を記述します。
わかりづらいと思いますので、具体例を示します。下記のプログラムは、1 行目 で data
に、3 つの要素 を持つ、反復可能オブジェクト である tuple を 代入 しています。
data = (1, 2, 3)
print(data)
print(*data)
print(1, 2, 3)
実行結果
(1, 2, 3)
1 2 3
1 2 3
上記のプログラムの 2 ~ 4 行目では下記の処理が行われます。
-
2 行目:
data
そのもの をprint
で表示しているので、data
に代入 された、(1, 2, 3) という tuple が表示 される -
3 行目:
data
の前に*
が記述されているので、反復可能オブジェクトの展開 が行われる。その結果、data
の要素 が 順番 に 取り出され、*data
の 部分 に それぞれ の 要素の値 が 位置引数 として記述されたprint(1, 2, 3)
が 実行 される。その結果、3 行目 と、4 行目 では、同じ表示 が行われる
なお、反復可能オブジェクトの展開 は、tuple だけでなく、下記のプログラムのように、list や dict に対しても行うことができます。なお、dict の場合は、キー が取り出されて 展開 されます。dict の キー と 値 を 展開 する方法については 後述 します。
data1 = [1, 2, 3]
data2 = { "a": 1, "b": 2 }
print(*data1)
print(*data2)
実行結果
1 2 3
a b
反復可能オブジェクトの展開を利用した dprint
の修正
先程の dprint
の問題は、下記のプログラムの 3 行目 のように、反復可能オブジェクトの展開 を print
の実引数に記述することで修正できます。
def dprint(debug, *args):
if debug:
print(*args)
修正箇所
def dprint(debug, *args):
if debug:
- print(args)
+ print(*args)
反復可能オブジェクトの展開 を 利用 することで、下記のプログラムのように、dprint
と print
で 同じ表示 が行われるようになります。なお、5、6 行目は、空行が表示 されます。
dprint(True, 1)
print(1)
dprint(True, 1, 2, 3)
print(1, 2, 3)
dprint(True)
print()
実行結果
1
1
1 2 3
1 2 3
*args
と反復可能オブジェクトの展開の関係
*args
と、反復可能オブジェクトの展開 は、どちらも 先頭 に 同じ *
という 記号 を 記述 するので 紛らわしい ですが、その 意味 は 異なる 点に 注意 して下さい。
見た目 は 紛らわしい ですが、記述する場所 などが下記のように 異なる ので、見分ける ことは 難しくない と思います。
-
*args
は 関数の定義 の 仮引数 の部分に 記述 される。名前 は 一般的 にargs
である - 反復可能オブジェクトの展開 は、関数呼び出し の 実引数 の部分に 記述 される。名前 は特に 決まっておらず、下記のプログラムのように、変数に代入されていない 反復可能オブジェクトに 対して も 反復可能オブジェクトの展開 を 記述 することが できる
print(*(1, 2, 3))
実行結果
1 2 3
両者 で 同じ記号 が 使われる のは、おそらく それぞれ で行われる 処理 が、下記のように 対応している からではないかと思います。
-
*args
の処理は、「位置引数 → 反復可能オブジェクト(tuple)」 - 反復可能オブジェクトの展開 の処理は、「反復可能オブジェクト → 位置引数」
キーワード引数に対する可変長引数
先程で定義した、dprint
には、まだ問題 があります。
その理由は、*args
は、キーワード引数 に 対応していない からです。そのため、下記のプログラムのように、仮引数 に *args
のみ が 記述 された関数に対して、キーワード引数 を 実引数 に 記述 して呼び出すと エラーが発生 します。
def a(*args):
print(args)
a(1, x=1)
実行結果
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[14], line 4
1 def a(*args):
2 print(args)
----> 4 a(1, x=1)
TypeError: a() got an unexpected keyword argument 'x'
上記のエラーメッセージは、以下のような意味を持ちます。
- TypeError
データ型(Type)に関するエラー
- a() got an unexpected keyword argument 'x'
a は、予期しない(unexpected)x というキーワード引数(keyword argument)を得た(got
そのため、dprint
で 改行を行わない 表示を行うために、下記のプログラムのように、print
と 同様 に キーワード引数 に end=""
を 記述 して呼び出すと エラーが発生 します。
dprint(True, 1, 2, 3, end="")
実行結果
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[125], line 1
----> 1 dprint(True, 1, 2, 3, end="")
TypeError: dprint() got an unexpected keyword argument 'end'
キーワード引数 に対する 可変長引数 は、仮引数 の 名前 の 先頭 に 半角の **
を記述します。キーワード引数 に対する 可変長引数 の 名前 には、kwargs
(keyword arguments の略)をつけるという 慣習 があります。以後は、*args
の場合と同様に、キーワード引数 に対する 可変長引数 を **kwargs
のように 表記 することにします。
**kwargs
は、対象が 位置引数 であるか、キーワード引数 であるか、代入する値が tuple であるか、dict であるかの 違いを除けば、*args
と 類似 する 性質 を持ちます。
具体的には、**kwargs
と、通常の仮引数 や デフォルト引数 を 併用 した場合は、キーワード引数 として 記述 された 実引数 は、以下の 仮引数 に代入されます。
- 対応 する 通常の仮引数 や デフォルト引数 が 存在すれば、その仮引数 に代入される
- それらが 存在しなければ、キーワード引数 の キーワード と 値 を、キー と その値 とする dict が、
kwargs
に代入 される
別の言葉で説明すると、「余ったキーワード引数 のデータを表す dict が kwargs
に代入 される」という処理が行われます。
下記は、*args
と **kwargs
の 違い を 表にまとめた ものです。
*args |
**kwargs |
|
---|---|---|
対応する実引数 | 位置引数 | キーワード引数 |
代入されるデータ型 | tuple | dict |
下記のプログラムは、仮引数 に *args
と **kwargs
を記述した関数を 呼び出した 例です。
-
5 行目:位置引数 である
1
、2
、3
の 値 が tuple に変換 されてargs
に代入 され、キーワード引数 であるx=4
とy=5
が dict に変換 されてkwargs
に代入 される -
6 行目:キーワード引数 を 記述せず に呼び出した場合は、
kwargs
には、空の dict が 代入 される
1 def a(*args, **kwargs):
2 print(args)
3 print(kwargs)
4
5 a(1, 2, 3, x=4, y=5)
6 a(6, 7, 8)
行番号のないプログラム
def a(*args, **kwargs):
print(args)
print(kwargs)
a(1, 2, 3, x=4, y=5)
a(6, 7, 8)
実行結果
(1, 2, 3)
{'x': 4, 'y': 5}
(6, 7, 8)
{}
マッピング型の展開
**kwargs
を使って、dprint
を下記のプログラムのように修正すれば良いと思うかもしれませんが、下記のプログラムは 意図通り の 処理 を 行いません。
-
1 行目:仮引数に
**kwargs
を追加した -
3 行目:
print
の実引数にkwargs
を追加した
def dprint(debug, *args, **kwargs):
if debug:
print(*args, kwargs)
修正箇所
-def dprint(debug, *args):
+def dprint(debug, *args, **kwargs):
if debug:
- print(*args)
+ print(*args, kwargs)
下記のプログラムは、dprint
を使って、"abc"
の 後 で 改行せず に "def"
を 表示しようと していますが、実行結果 から、それぞれの行 で、"abc"
や "def"
の後に、kwargs
に代入された dict が表示 されてしまいます。
dprint(True, "abc", end="")
dprint(True, "def")
実行結果
abc {'end': ''}
def {}
この問題は、以前の記事で説明した、マッピング型の展開 を利用することで 解決 できます。以前の記事では dict の展開 と表記しましたが、マッピング型の展開 のほうが 正確 なので、以後はそちらの表記を用います。なお、マッピング型 とは、dict や defaultdict など、キー と キーの値 によって 複数 の オブジェクト を 管理 する データ型 のことです。
マッピング型の展開 とは、マッピング型 のデータの キー と キーの値 を、キーワード引数に変換 したものと みなして、関数呼び出しを行う という処理です。マッピング型の展開 は、実引数 を 記述 する際に、マッピング型 のデータの 先頭 に 半角の **
を記述します。
dprint
で、**kwargs
に代入された dict の キー と キーの値 を、キーワード引数 として print
の 実引数 で 利用する 場合は、下記のプログラムの 3 行目のように、マッピング型の展開 を表す **kwargs
を print
の 実引数 に 記述 します。
def dprint(debug, *args, **kwargs):
if debug:
print(*args, **kwargs)
修正箇所
def dprint(debug, *args, **kwargs):
if debug:
- print(*args, kwargs)
+ print(*args, **kwargs)
修正 した dprint
に対して、下記の 先ほどと同じ プログラムを 実行 すると、1 行目 の処理によって "abc"
が 改行せず に 表示 されるため、意図通り の 表示 が行われます。
dprint(True, "abc", end="")
dprint(True, "def")
実行結果
abcdef
**kwargs
と、マッピング型の展開 の 関係 は、*args
と、反復可能オブジェクトの展開 の関係と 同様 なので、その説明は省略します。
これで、デバッグ表示を行うための dprint
は 完成 です。
可変長引数と実引数の展開に関する補足
以後は、反復可能オブジェクトの展開 と、マッピング型の展開 の 総称 を、実引数の展開 と表記することにします。
以下、可変長引数 と、実引数の展開 に関するいくつかの 補足説明 を行います。
可変長引数と実引数の展開の関係のまとめ
可変長引数 と 実引数の展開 の 関係 を まとめる と、以下のようになります。
記述場所 | 行う処理 | |
---|---|---|
*args |
仮引数 | 位置引数 → 反復可能オブジェクト(tuple) |
反復可能オブジェクトの展開 | 実引数 | 反復可能オブジェクト → 位置引数 |
**kwargs |
仮引数 | キーワード引数 → マッピング型(dict) |
マッピング型の展開 | 実引数 | マッピング型 → キーワード引数 |
上記の 4 つは、下記の関係にあります。
- 可変長引数 と、展開 は 逆の処理 を行う
- 「
*args
と**kwargs
」、「反復可能オブジェクトの展開 と マッピング型の展開」は、前者が 位置引数 を、後者が キーワード引数 を扱うことを 除く と 同様の処理 を行う
*args
、**kwargs
、通常の仮引数、デフォルト引数の併用
*args
、**kwargs
、通常の仮引数、デフォルト引数 は、すべて併用することができます。
併用 した場合は、通常の仮引数 と デフォルト引数 に 対応 する 実引数の値 は、*args
にも **kwargs
にも 代入されません。具体例を挙げて説明します。下記のプログラムは、通常の仮引数 x
、デフォルト引数 y
と z
、*args
、**kwargs
を 仮引数 として持ち、それぞれの 仮引数の値 を print
で表示 する 関数を定義 しています。
def a(x, y=2, z=3, *args, **kwargs):
print(x, y, z)
print(args)
print(kwargs)
この関数に対して下記のプログラムを実行すると、下記の処理が行われます。
-
位置引数
1
が 対応 する 通常の仮引数x
に 代入 される -
キーワード引数
z=5
によって、対応 する デフォルト引数z
に5
が 代入 される。 -
キーワード引数
b=5
に 対応 する通常の仮引数もデフォルト引数も 存在しない ので、{"b": 5}
という dict がkwargs
に 代入 される -
デフォルト引数
y
に 対応 する実引数が 存在しない ので、y
には デフォルト値 である2
が 代入 される -
すべて の 位置引数 に 対応 する 仮引数 が 存在 するので、
args
には、空の tuple が 代入 される
a(1, z=5, b=3)
実行結果
1 2 5
()
{'b': 3}
上記をまとめると、以下のようになります
*args
、**kwargs
、通常の仮引数、デフォルト引数 を 併用 した場合は、*args
と **kwargs
には、通常の仮引数 と デフォルト引数 に 代入 された 値 は 入らない。
仮引数の記述に関するルール
*args
、**kwargs
、通常の仮引数、デフォルト引数 を 併用 する場合は、下記の ルール があります。ルール 1、2、3 を 破る と エラーが発生 します。ルール 4、5 は少し 細かい話 になりますが、思わぬバグの原因 になる 可能性 があるので 注意 が必要です。
- 通常の仮引数 は、それ以外 の仮引数よりも 前に記述 する必要がある
-
*args
と**kwargs
は、いずれ も 2 つ以上記述 することは できない -
*args
は**kwargs
よりも 前に記述 する必要がある - デフォルト引数 は、通常の仮引数より後 であれば、任意の場所 に 記述 できる
-
デフォルト引数 を
*args
や**kwargs
より後 に 記述 した場合は、その デフォルト引数 の 値 を、位置引数 に 記述 して 代入 することは できなくなる
上記の ルール 4、5 が 分かりづらい と思いますので、具体例を挙げて説明します。
下記の a
と b
は、いずれも 同じ仮引数 を持つ 関数 ですが、デフォルト引数 y=2
と、*args
の 記述の順番 が 異なり ます。
def a(x, y=3, *args, **kwargs):
print(x, y)
print(args)
print(kwargs)
def b(x, *args, y=3, **kwargs):
print(x, y)
print(args)
print(kwargs)
下記のプログラムのように、a
と b
に対して 1
、y=3
の 順番 で 実引数 を 記述 して呼び出した場合は、a
と b
は 同じ内容 を 表示 します。その 理由 は以下の通りです。
-
a
もb
も 最初 に 記述 した 位置引数1
に 対応 する 通常の仮引数x
が 存在する ので、x
には どちらも1
が 代入 される -
a
もb
も、2 つ目 に 記述 した キーワード引数y=5
に 対応 する、デフォルト引数y
が 存在 するので、デフォルト引数y
には どちらも2
が 代入 される -
*args
と**args
に 対応 する 実引数 はどちらも 記述されていない ので、どちらもargs
には 空の tuple が、kwargs
には 空の dict が 代入 される
a(1, y=5)
b(1, y=5)
実行結果
1 5
()
{}
1 5
()
{}
一方、下記のプログラムのように、a
と b
に対して、1
、2
の 位置引数 のみを 記述 した場合は、下記のように 異なる表示 が行われます。その 理由 は以下の通りです。
-
a
もb
も 最初 に 記述 した 位置引数1
に 対応 する 通常の仮引数x
が 存在する ので、x
には どちらも1
が 代入 される -
a
には、2 つ目 に 記述 された 位置引数2
に 対応 する デフォルト引数y
が 存在 するので、a
のy
には2
が 代入 される -
a
の*args
に 対応 する 実引数 は 記述されていない ので、a
のargs
には 空の tuple が 代入 される -
b
には、2 つ目 に 記述 された 位置引数2
に 対応 する、通常の仮引数 または デフォルト引数 が 存在しない ので、b
のargs
の tuple の 0 番 の 要素 に2
が 代入 される -
b
の デフォルト引数y
に 対応 する 実引数 が 記述されていない ので、b
のy
には デフォルト値 である3
が 代入 される -
**args
に 対応 する 実引数 は どちらも記述されていない ので、どちらもkwargs
には 空の dict が 代入 される
a(1, 2)
b(1, 2)
実行結果
1 2
()
{}
1 3
(2,)
{}
上記からわかるように、デフォルト引数 を、*args
や **kwargs
より 後に記述 した場合は、その デフォルト引数 の 値 を、位置引数 に 記述 して 代入できなくなります。
展開に関する補足
*args
と **kwargs
に関する ルール と 勘違いする 人がいるかもしれませんが、反復可能オブジェクトの展開 と、マッピング型の展開 は、展開した後 の、すべて の 位置引数 が キーワード引数 よりも 前に記述 されていれば、下記のプログラムの 5 行目のように、任意の場所 に、いくつでも記述 することが できます。なお、下記のプログラムの 6 行目は、5 行目と同じ処理を行うプログラムです。
1 def a(*args, **kwargs):
2 print(args)
3 print(kwargs)
4
5 a(1, *(2, 3), *(4, 5, 6), **{"a": 7, "b": 8}, **{"c": 9})
6 a(1, 2, 3, 4, 5, 6, a=7, b=8, c=9)
行番号のないプログラム
def a(*args, **kwargs):
print(args)
print(kwargs)
a(1, *(2, 3), *(4, 5, 6), **{"a": 7, "b": 8}, **{"c": 9})
a(1, 2, 3, 4, 5, 6, a=7, b=8, c=9)
実行結果
(1, 2, 3, 4, 5, 6)
{'a': 7, 'b': 8, 'c': 9}
(1, 2, 3, 4, 5, 6)
{'a': 7, 'b': 8, 'c': 9}
下記のプログラムは、展開した後で、キーワード引数が位置引数よりも前に記述されるのでエラーが発生します。なお、下記の 1 行目と 2 行目は同じ処理を行うプログラムです。
a(*(1, 2), **{"a": 3}, *(4, 5, 6))
a(1, 2, a=3, 4, 5, 6)
実行結果
Cell In[27], line 1
a(*(1, 2), **{"a": 3}, *(4, 5, 6))
^
SyntaxError: iterable argument unpacking follows keyword argument unpacking
上記のエラーメッセージは、以下のような意味を持ちます。
-
SyntaxError
構文(文法のこと)(syntax)に関するエラー -
iterable argument unpacking follows keyword argument unpacking
反復可能(iterable)な実引数(argument)の展開(unpacking)が、キーワード引数(keyword argument)の展開(unpacking)(マッピング型の展開のこと)の後(follows)に記述されている
ai_by_score
の改良(デバッグ表示の機能の追加)
デバッグ表示 を行う dprint
を使って ai_by_score
に デバッグ表示 の 機能を追加 します。まず、下記のプログラムの 1 行目 のように、以前の記事 で実装した test_judge
と 同様の方法 で、デバッグ表示 を 行うか どうかを 選択 するための、デフォルト引数 debug
を、デフォルト値 に False
を 設定 して 追加 します。
1 from copy import deepcopy
2 from random import choice
3
4 def ai_by_score(mb_orig, eval_func, debug=False):
5 dprint(debug, "Start ai_by_score")
6 dprint(debug, mb_orig)
7 legal_moves = mb_orig.calc_legal_moves()
8 dprint(debug, "legal_moves", legal_moves)
9 best_score = float("-inf")
10 best_moves = []
11 for move in legal_moves:
12 dprint(debug, "=" * 20)
13 dprint(debug, "move", move)
14 mb = deepcopy(mb_orig)
15 x, y = move
16 mb.move(x, y)
17 dprint(debug, mb)
18
19 score = eval_func(mb)
20 dprint(debug, "score", score, "best score", best_score)
21
22 if best_score < score:
23 best_score = score
24 best_moves = [move]
25 dprint(debug, "UPDATE")
26 dprint(debug, " best score", best_score)
27 dprint(debug, " best moves", best_moves)
28 elif best_score == score:
29 best_moves.append(move)
30 dprint(debug, "APPEND")
31 dprint(debug, " best moves", best_moves)
32
33 dprint(debug, "=" * 20)
34 dprint(debug, "Finished")
35 dprint(debug, "best score", best_score)
36 dprint(debug, "best moves", best_moves)
37 return choice(best_moves)
行番号のないプログラム
from copy import deepcopy
from random import choice
def ai_by_score(mb_orig, eval_func, debug=False):
dprint(debug, "Start ai_by_score")
dprint(debug, mb_orig)
legal_moves = mb_orig.calc_legal_moves()
dprint(debug, "legal_moves", legal_moves)
best_score = float("-inf")
best_moves = []
for move in legal_moves:
dprint(debug, "=" * 20)
dprint(debug, "move", move)
mb = deepcopy(mb_orig)
x, y = move
mb.move(x, y)
dprint(debug, mb)
score = eval_func(mb)
dprint(debug, "score", score, "best score", best_score)
if best_score < score:
best_score = score
best_moves = [move]
dprint(debug, "UPDATE")
dprint(debug, " best score", best_score)
dprint(debug, " best moves", best_moves)
elif best_score == score:
best_moves.append(move)
dprint(debug, "APPEND")
dprint(debug, " best moves", best_moves)
dprint(debug, "=" * 20)
dprint(debug, "Finished")
dprint(debug, "best score", best_score)
dprint(debug, "best moves", best_moves)
return choice(best_moves)
修正箇所
from copy import deepcopy
from random import choice
-def ai_by_score(mb_orig, eval_func):
+def ai_by_score(mb_orig, eval_func, debug=False):
+ dprint(debug, "Start ai_by_score")
+ dprint(debug, mb_orig)
legal_moves = mb_orig.calc_legal_moves()
+ dprint(debug, "legal_moves", legal_moves)
best_score = float("-inf")
best_moves = []
for move in legal_moves:
+ dprint(debug, "=" * 20)
+ dprint(debug, "move", move)
mb = deepcopy(mb_orig)
x, y = move
mb.move(x, y)
+ dprint(debug, mb)
score = eval_func(mb)
+ dprint(debug, "score", score, "best score", best_score)
if best_score < score:
best_score = score
best_moves = [move]
+ dprint(debug, "UPDATE")
+ dprint(debug, " best score", best_score)
+ dprint(debug, " best moves", best_moves)
elif best_score == score:
best_moves.append(move)
+ dprint(debug, "APPEND")
+ dprint(debug, " best moves", best_moves)
+ dprint(debug, "=" * 20)
+ dprint(debug, "Finished")
+ dprint(debug, "best score", best_score)
+ dprint(debug, "best moves", best_moves)
return choice(best_moves)
デバッグ表示 では、ai_by_score
で行われた 処理の経過 を dprint
を使って、以下のように 表示 します。なお、デバッグ表示 で 何を表示するか は、どのような処理の経過 を 知りたいか によって 変わります。下記のプログラムで表示する以外のデバッグ表示を行いたい人は、自由にデバッグ表示 を 追加 して下さい。また、下記の 表示の仕方 が 分かりづらい と 思った方 は 自由に変更 して下さい。
-
5 行目:
ai_by_score
の 処理を開始 したことを 表示 する - 6 行目:処理 を 開始した時点 の ゲーム盤 を 表示 する
- 8 行目:合法手の一覧 を 表 示する
-
11 行目:新しい着手 を 行う ことを 明確にする ために、
=
を 20 個並べて表示 する - 12 行目:着手 する 合法手 を 表示 する
- 17 行目:着手後 の ゲーム盤 を 表示 する
- 20 行目:計算した 評価値 と、それまでの評価値 の 最大値 を 表示 する
-
25 ~ 27 行目:評価値 が、それまでの評価値 の 最大値 を 超えた ことによって、
best_score
とbest_moves
が 更新(update)したことと、更新後 の 値 を 表示 する -
30 ~ 31 行目:評価値 が、それまでの評価値 の 最大値 と 同じ であったので、
best_moves
に 着手 を 追加(append)te)したことと、追加後 の 値 を 表示 する -
35 ~ 38 行目:
ai_by_score
の 処理が完了 したことと、評価値の最大値 と、計算された 最善手の一覧 を 表示 する
ai2s
と ai3s
の修正
次に、ai2s
に デバッグ表示 の 機能 を 追加 するために、下記のプログラムのように修正します。修正箇所は、デフォルト引数 debug
を 追加 した点と、ai_by_score
に キーワード引数 debug=debug
を 追加 した点です。
def ai2s(mb, debug=False):
def eval_func(mb):
return 0
return ai_by_score(mb, eval_func, debug=debug)
修正箇所
-def ai2s(mb):
+def ai2s(mb, debug=False):
def eval_func(mb):
return 0
- return ai_by_score(mb, eval_func)
+ return ai_by_score(mb, eval_func, debug=debug)
5 行目 を、ai_by_score(mb, eval_func, debug)
のように、キーワード引数ではなく、位置引数 で 記述 することも できます が、デバッグ表示 を 行うか どうかを表す 実引数 は、その 意味を明確 に するため に、キーワード引数 で 記述することが多い ような気がするので、キーワード引数で記述 しました。
ai3s
も 同様の方法 で、下記のプログラムのように修正します。
def ai3s(mb, debug=False):
def eval_func(mb):
if mb.last_move == (1, 1):
return 1
else:
return 0
return ai_by_score(mb, eval_func, debug=debug)
修正箇所
-def ai3s(mb):
+def ai3s(mb, debug=False):
def eval_func(mb):
if mb.last_move == (1, 1):
return 1
else:
return 0
- return ai_by_score(mb, eval_func)
+ return ai_by_score(mb, eval_func, debug=debug)
動作の確認
修正した ai2s
が 正しく動作 するかどうかを 確認 するために、ai2
と 対戦 を行います。修正した ai2s
の デフォルト引数 debug
の デフォルト値 は、False
であり、ai_match
の ブロックの中 では、AI の関数 を 呼び出す際 に、debug
に関する 実引数 を 記述していない ので、ai_match
を 実行 しても デバッグ表示 は 行われません。そのため、下記のプログラムのように、これまでと同様 の 記述 で ai_match
による 対戦 を、デバッグ表示なし で 行う ことができます。実行結果 から、ai2s
を 正しく実装 できていることが 確認 できます。
from ai import ai_match, ai2, ai3
ai_match(ai=[ai2s, ai2])
実行結果(実行結果はランダムなので下記とは異なる場合があります)
ai2s VS ai2
count win lose draw
o 5921 2868 1211
x 2923 5830 1247
total 8844 8698 2458
ratio win lose draw
o 59.2% 28.7% 12.1%
x 29.2% 58.3% 12.5%
total 44.2% 43.5% 12.3%
ai3s
も、下記のプログラムのように 同様の方法 で 正しく動作 することが 確認 できます。
ai_match(ai=[ai3s, ai3])
実行結果(実行結果はランダムなので下記とは異なる場合があります)
ai3s VS ai3
count win lose draw
o 6848 1945 1207
x 1849 7095 1056
total 8697 9040 2263
ratio win lose draw
o 68.5% 19.4% 12.1%
x 18.5% 71.0% 10.6%
total 43.5% 45.2% 11.3%
デバッグ表示を使った ai3s
の処理の流れの確認
デバッグ表示 は、バグを修正 する だけでなく、処理の流れ を 確認したい 場合でも 利用できます。せっかくなので、下記のプログラムを実行することで ai3s
の デバッグ表示 を行い、評価値を利用 した アルゴリズム の 処理の流れ を 確認 することにします。
from marubatsu import Marubatsu
mb = Marubatsu()
ai3s(mb, debug=True)
実行結果
Start ai_by_score
Turn o
...
...
...
legal_moves [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
====================
move (0, 0)
Turn x
O..
...
...
score 0 best score -inf
UPDATE
best score 0
best moves [(0, 0)]
====================
move (1, 0)
Turn x
.O.
...
...
score 0 best score 0
APPEND
best moves [(0, 0), (1, 0)]
====================
move (2, 0)
Turn x
..O
...
...
score 0 best score 0
APPEND
best moves [(0, 0), (1, 0), (2, 0)]
====================
move (0, 1)
Turn x
...
O..
...
score 0 best score 0
APPEND
best moves [(0, 0), (1, 0), (2, 0), (0, 1)]
====================
move (1, 1)
Turn x
...
.O.
...
score 1 best score 0
UPDATE
best score 1
best moves [(1, 1)]
====================
move (2, 1)
Turn x
...
..O
...
score 0 best score 1
====================
move (0, 2)
Turn x
...
...
O..
score 0 best score 1
====================
move (1, 2)
Turn x
...
...
.O.
score 0 best score 1
====================
move (2, 2)
Turn x
...
...
..O
score 0 best score 1
====================
Finished
best moves [(1, 1)]
実行結果 の 表示 を まとめる と以下の 表 のようになります。
表から、score
が best_score
より大きい、(0, 0) と (1, 1) の 着手 で 更新の処理 が行われ、score
が best_score
と 等しい、(1, 0)、(2, 0)、(0, 1) の 着手 で best_moves
に 着手 を 追加 する 処理 が 行われる ことが分かります。また、(1, 1) の 着手以降 では、評価値 が 1
以上 に なることは無い ので、最善手の一覧 が (1, 1) のみになることが分かります。
着手 | score |
best_score |
best_moves |
処理 |
---|---|---|---|---|
開始時 | 負の無限大 | [] |
||
(0, 0) | 0 |
0 |
[(0,0)] |
更新 |
(1, 0) | 0 |
0 |
[(0,0),(1,0)] |
追加 |
(2, 0) | 0 |
0 |
[(0,0),(1,0),(2,0)] |
追加 |
(0, 1) | 0 |
0 |
[(0,0),(1,0),(2,0),(1,0)] |
追加 |
(1, 1) | 1 |
1 |
[(1,1)] |
更新 |
(2, 1) | 0 |
1 |
[(1,1)] |
|
(0, 2) | 0 |
1 |
[(1,1)] |
|
(1, 2) | 0 |
1 |
[(1,1)] |
|
(2, 2) | 0 |
1 |
[(1,1)] |
評価値によるアルゴリズムで行われる処理の流れが良くわからなかった人でも、デバッグ表示 を見ることで、処理の流れ が 理解できる ようになったのではないでしょうか?
今回の記事のまとめ
今回の記事では、評価値を利用 した アルゴリズム の AI で、デバッグ表示 を行うことができるように ai_by_score
を 改良 しました。また、その際に必要となる知識として、可変長引数 と、実引数の展開 について説明しました。
次回の記事では、評価値を利用 した アルゴリズム で、ルール 1、4 を 実装 する方法について説明します。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
今回の記事では、marubatsu.py は修正していないので、marubatsu_new.py はありません。
以下のリンクは、今回の記事で更新した ai.py です。
次回の記事