0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonで〇×ゲームのAIを一から作成する その48 デバッグ表示と、可変長引数と実引数の展開

0
Last updated at Posted at 2024-01-25

目次と前回の記事

これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

これまでに作成した 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 の 頭文字 をとって dprint1 という 名前 にする
入力:デバッグ表示 を 行うか どうかを 表す値 を 仮引数 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 は少し 細かい話 になりますが、思わぬバグの原因 になる 可能性 があるので 注意 が必要です。

  1. 通常の仮引数 は、それ以外 の仮引数よりも 前に記述 する必要がある
  2. *args と **kwargs は、いずれ も 2 つ以上記述 することは できない
  3. *args は **kwargs よりも 前に記述 する必要がある
  4. デフォルト引数 は、通常の仮引数より後 であれば、任意の場所 に 記述 できる
  5. デフォルト引数 を *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 です。

次回の記事

  1. さらに省略したい 場合は、dp という名前を付けると良いでしょう ↩

  2. 慣習なので 別の名前 にしても かまいません が、他の人 がプログラムを見た時に 分かりづらくなる ので 避けたほうが良い でしょう ↩

  3. すぐ上で説明したように、仮引数の名前 は *args ではなく、args です ↩

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?