0
0

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

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 行目debugTrue の場合 に、printdata を表示 する
def dprint(debug, data):
    if debug:
        print(data)

下記のプログラムは、dprint利用例 です。1 行目 では、dprint1 つ目実引数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仮引数debugdata2 つしかない にもかかわらず、True123 つ実引数記述 して 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 行目 のように、実引数の数10場合 でも、必ず 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 に代入 される

別の言葉で説明すると、「余った位置引数要素 として持つ tupleargs に代入 される」という処理が行われます。

従って、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 だけでなく、下記のプログラムのように、listdict に対しても行うことができます。なお、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)

反復可能オブジェクトの展開利用 することで、下記のプログラムのように、dprintprint同じ表示 が行われるようになります。なお、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 に代入 される

別の言葉で説明すると、「余ったキーワード引数 のデータを表す dictkwargs に代入 される」という処理が行われます。

下記は、*args**kwargs違い表にまとめた ものです。

*args **kwargs
対応する実引数 位置引数 キーワード引数
代入されるデータ型 tuple dict

下記のプログラムは、仮引数*args**kwargs を記述した関数を 呼び出した 例です。

  • 5 行目位置引数 である 123tuple に変換 されて args に代入 され、キーワード引数 である x=4y=5dict に変換 されて 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 の展開 と表記しましたが、マッピング型の展開 のほうが 正確 なので、以後はそちらの表記を用います。なお、マッピング型 とは、dictdefaultdict など、キーキーの値 によって 複数オブジェクト管理 する データ型 のことです。

マッピング型の展開 とは、マッピング型 のデータの キーキーの値 を、キーワード引数に変換 したものと みなして関数呼び出しを行う という処理です。マッピング型の展開 は、実引数記述 する際に、マッピング型 のデータの 先頭半角の ** を記述します。

dprint で、**kwargs に代入された dictキーキーの値 を、キーワード引数 として print実引数利用する 場合は、下記のプログラムの 3 行目のように、マッピング型の展開 を表す **kwargsprint実引数記述 します。

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デフォルト引数 yz*args**kwargs仮引数 として持ち、それぞれの 仮引数の値print で表示 する 関数を定義 しています。

def a(x, y=2, z=3, *args, **kwargs):
    print(x, y, z)
    print(args)
    print(kwargs)

この関数に対して下記のプログラムを実行すると、下記の処理が行われます。

  • 位置引数 1対応 する 通常の仮引数 x代入 される
  • キーワード引数 z=5 によって、対応 する デフォルト引数 z5代入 される。
  • キーワード引数 b=5対応 する通常の仮引数もデフォルト引数も 存在しない ので、{"b": 5} という dictkwargs代入 される
  • デフォルト引数 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分かりづらい と思いますので、具体例を挙げて説明します。

下記の ab は、いずれも 同じ仮引数 を持つ 関数 ですが、デフォルト引数 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)

下記のプログラムのように、ab に対して 1y=3順番実引数記述 して呼び出した場合は、ab同じ内容表示 します。その 理由 は以下の通りです。

  • ab最初記述 した 位置引数 1対応 する 通常の仮引数 x存在する ので、x には どちらも 1代入 される
  • ab も、2 つ目記述 した キーワード引数 y=5対応 する、デフォルト引数 y存在 するので、デフォルト引数 y には どちらも 2代入 される
  • *args**args対応 する 実引数 はどちらも 記述されていない ので、どちらも args には 空の tuple が、kwargs には 空の dict代入 される
a(1, y=5)
b(1, y=5)

実行結果

1 5
()
{}
1 5
()
{}

一方、下記のプログラムのように、ab に対して、12位置引数 のみを 記述 した場合は、下記のように 異なる表示 が行われます。その 理由 は以下の通りです。

  • ab最初記述 した 位置引数 1対応 する 通常の仮引数 x存在する ので、x には どちらも 1代入 される
  • a には、2 つ目記述 された 位置引数 2対応 する デフォルト引数 y存在 するので、ay には 2代入 される
  • a*args対応 する 実引数記述されていない ので、aargs には 空の tuple代入 される
  • b には、2 つ目記述 された 位置引数 2対応 する、通常の仮引数 または デフォルト引数存在しない ので、bargstuple0 番要素2代入 される
  • bデフォルト引数 y対応 する 実引数記述されていない ので、by には デフォルト値 である 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_scorebest_moves更新(update)したことと、更新後表示 する
  • 30 ~ 31 行目評価値 が、それまでの評価値最大値同じ であったので、best_moves着手追加(append)te)したことと、追加後表示 する
  • 35 ~ 38 行目ai_by_score処理が完了 したことと、評価値の最大値 と、計算された 最善手の一覧表示 する

ai2sai3s の修正

次に、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)]

実行結果表示まとめる と以下の のようになります。

表から、scorebest_score より大きい(0, 0)(1, 1)着手更新の処理 が行われ、scorebest_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改良 しました。また、その際に必要となる知識として、可変長引数 と、実引数の展開 について説明しました。

次回の記事では、評価値を利用 した アルゴリズム で、ルール 14実装 する方法について説明します。

本記事で入力したプログラム

以下のリンクから、本記事で入力して実行した 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