目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
test.py | テストに関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
ラッパー関数とラップする関数の仮引数
前回の記事では、ai_by_score
をデコレーター式に記述 して、局面の評価値を計算して着手を選択する AI を定義 しました。ai_by_score
が定義した ラップする関数 は以下のような性質を持ちます。
-
仮引数
mb
に評価値を計算する 局面を表すデータを代入 する -
mb
の局面の評価値 を計算して 返り値として返す
下記は前回の記事でデコレーター式に @ai_by_score
を記述して定義した ai2s
のプログラムです。
@ai_by_score
def ai2s(mb):
return 0
また、デコレーターが定義する ラッパー関数 は以下のような性質を持ちます。
-
ラップする関数を呼び出す際に必要 となる
mb
を計算するために必要となる 仮引数mb_orig
を持つ - 仮引数として、ラッパー関数のみで利用 する
debug
、rand
、analyze
を持つ
下記は、デコレーターの関数 ai_by_score
の定義で、ラッパー関数の定義は 8 行目 で行われています。また、ラッパー関数の中では 11 ~ 13 行目の処理で mb_orig
から ラップする関数を呼び出す際に実引数に記述する mb
を作成 して ラップする関数を呼び出しています。
1 from ai import dprint
2 from functools import wraps
3 from copy import deepcopy
4 from random import choice
5
6 def ai_by_score(eval_func):
7 @wraps(eval_func)
8 def wrapper(mb_orig, debug=False, rand=True, analyze=False):
9 dprint(debug, "Start ai_by_score")
省略
10 for move in legal_moves:
省略
11 mb = deepcopy(mb_orig)
12 x, y = move
13 mb.move(x, y)
14 dprint(debug, mb)
15
16 score = eval_func(mb)
省略
17 return wrapper
行番号のないプログラム
from ai import dprint
from functools import wraps
from copy import deepcopy
from random import choice
def ai_by_score(eval_func):
@wraps(eval_func)
def wrapper(mb_orig, debug=False, rand=True, analyze=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 = []
if analyze:
score_by_move = {}
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 analyze:
score_by_move[move] = 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)
if analyze:
return {
"candidate": best_moves,
"score_by_move": score_by_move,
}
elif rand:
return choice(best_moves)
else:
return best_moves[0]
return wrapper
上記の ラッパー関数の仮引数 は以下のように 2 種類に分類 できます。
-
ラップする関数 を呼び出す際に記述する実引数に 関連する 仮引数
mb_orig
-
ラッパー関数のみで利用 する仮引数
debug
、rand
、analyze
mb
以外の仮引数を持つ AI の関数のラッパー関数
評価値を計算することで着手を選択する AI の関数には、下記のプログラムの ai11s
のように、mb
以外を仮引数として持つ ものがあります。このような関数に対しては、上記の ai_by_score
をデコレーターとして 利用することはできません。
def ai11s(mb, score_201=2, score_102=0.5, score_012=-1, debug=False):
def eval_func(mb):
略
return score
return ai_by_score(mb, eval_func, debug=debug)
下記のプログラムで、前回の記事でデコレーター式を使って ai2s
を定義しなおしたのと同様の方法で ai11s
を定義しなおしても、この時点ではエラーは発生しません。
from marubatsu import Marubatsu, Markpat
from pprint import pprint
@ai_by_score
def ai11s(mb):
元の ai11s の中の eval_func と同じなので略
return score
プログラム全体
from marubatsu import Marubatsu, Markpat
from pprint import pprint
@ai_by_score
def ai11s(mb):
# 真ん中のマスに着手している場合は、評価値として 300 を返す
if mb.last_move == (1, 1):
return 300
# 自分が勝利している場合は、評価値として 200 を返す
if mb.status == mb.last_turn:
return 200
markpats = mb.count_markpats()
if debug:
pprint(markpats)
# 相手が勝利できる場合は評価値として -100 を返す
if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
return -100
# 次の自分の手番で自分が必ず勝利できる場合は評価値として 100 を返す
elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
return 100
# 評価値の合計を計算する変数を 0 で初期化する
score = 0
# 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
score += score_201
# 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
# 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
# 計算した評価値を返す
return score
しかし、上記の実行後に下記のプログラムで、ゲーム開始時の局面に対して ai11s
で着手を計算 しようとすると、実行結果のように エラーが発生 します。このエラーの原因について少し考えてみて下さい。
mb = Marubatsu()
print(ai11s(mb))
実行結果
略
Cell In[2], line 15
12 return 200
14 markpats = mb.count_markpats()
---> 15 if debug:
16 pprint(markpats)
17 # 相手が勝利できる場合は評価値として -100 を返す
NameError: name 'debug' is not defined
エラーの原因の検証
エラーメッセージから、上記のエラーは debug
という変数が定義されていない ことが原因である事がわかります。修正前のプログラム にも debug
を利用する処理 は下記の 3、4 行目のように 記述されています が、この debug
は 1 行目で ai11s
の仮引数 として定義されているため、eval_func
の クロージャー変数 として 利用できる のでエラーは発生しません。
1 def ai11s(mb, score_201=2, score_102=0.5, score_012=-1, debug=False):
2 def eval_func(mb):
略
3 if debug:
4 pprint(markpats)
略
5 return score
6
7 return ai_by_score(mb, eval_func, debug=debug)
一方、デコレーター式 によって定義された ai11s
を呼び出す と、下記の ai_by_score
の 3 行目で定義された ラッパー関数 wrapper
が呼び出されます。
1 def ai_by_score(eval_func):
2 @wraps(eval_func)
3 def wrapper(mb_orig, debug=False, rand=True, analyze=False):
省略
4 score = eval_func(mb)
省略
5 return wrapper
また、上記の 4 行目 で、下記の 2 ~ 5 行目で定義された ラップする関数が呼び出されて、下記の 3 行目の処理が実行されますが、下記の 2 ~ 5 行目で定義された ラップする関数 は、グローバル関数として定義 されているため、debug
というクロージャー変数は存在しません。そのため、先程のように、debug
が定義されていないというエラーが発生します。
1 @ai_by_score
2 def ai11s(mb):
略
3 if debug:
4 pprint(markpats)
略
5 return score
ai_by_score
と ai11s
の修正
このように、デコレーター式でラップする関数 はラッパー関数の ローカル関数として定義されていない ので、その中の処理で ラッパー関数のローカル変数 をクロージャー変数として 利用することはできません。そのため、ラッパー関数のローカル変数 を ラップする関数で利用する ためには、ai11s
の仮引数 mb
のように、ラップする関数に仮引数を用意 して、その データーを受け渡す 必要があります。
そこで、ai_by_score
を下記のプログラムのように修正します。
-
4 行目:ラップする関数を呼び出す際に、実引数
debug=debug
を記述する
1 def ai_by_score(eval_func):
2 @wraps(eval_func)
3 def wrapper(mb_orig, debug=False, rand=True, analyze=False):
元と同じなので省略
4 score = eval_func(mb, debug=debug)
元と同じなので省略
5
6 return wrapper
行番号のないプログラム
def ai_by_score(eval_func):
@wraps(eval_func)
def wrapper(mb_orig, debug=False, rand=True, analyze=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 = []
if analyze:
score_by_move = {}
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, debug=debug)
dprint(debug, "score", score, "best score", best_score)
if analyze:
score_by_move[move] = 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)
if analyze:
return {
"candidate": best_moves,
"score_by_move": score_by_move,
}
elif rand:
return choice(best_moves)
else:
return best_moves[0]
return wrapper
修正箇所
def ai_by_score(eval_func):
@wraps(eval_func)
def wrapper(mb_orig, debug=False, rand=True, analyze=False):
元と同じなので省略
- score = eval_func(mb)
+ score = eval_func(mb, debug=debug)
元と同じなので省略
return wrapper
次に、ai11s
を下記のプログラムのように修正します。
-
2 行目:仮引数
debug
を追加する。元のai11s
の定義に倣って、デフォルト値をFalse
とするデフォルト引数とした
1 @ai_by_score
2 def ai11s(mb, debug=False):
元と同じなので省略
3 return score
行番号のないプログラム
@ai_by_score
def ai11s(mb, debug=False):
# 真ん中のマスに着手している場合は、評価値として 300 を返す
if mb.last_move == (1, 1):
return 300
# 自分が勝利している場合は、評価値として 200 を返す
if mb.status == mb.last_turn:
return 200
markpats = mb.count_markpats()
if debug:
pprint(markpats)
# 相手が勝利できる場合は評価値として -100 を返す
if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
return -100
# 次の自分の手番で自分が必ず勝利できる場合は評価値として 100 を返す
elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
return 100
# 評価値の合計を計算する変数を 0 で初期化する
score = 0
# 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
score += score_201
# 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
# 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
# 計算した評価値を返す
return score
修正箇所
@ai_by_score
-def ai11s(mb):
+def ai11s(mb, debug=False):
元と同じなので省略
return score
ai11s
を定義する際 にデコレーター式 @ai_by_score
を利用 しているので、ai11s
よりも前 に、ai_by_score
の修正を行う必要がある 点に注意して下さい。
上記の修正後に下記のプログラムを実行すると、実行結果のような 別のエラーが発生 します。これは、先程の debug
と同様に score_102
が定義されていない ことが原因です。
print(ai11s(mb))
実行結果
略
Cell In[5], line 27
25 score += score_201
26 # 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
---> 27 score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
28 # 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
29 score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
NameError: name 'score_102' is not defined
ラップする関数が共通して持たない仮引数
先程の debug
は、評価値を計算する際のデバッグ表示を行うかどうかを表す変数なので、すべての 評価値を計算する ラップする関数に共通する仮引数 とすることができます。
一方、上記の score_102
は、ai11s
が独自に必要 とする仮引数です。ラップする関数 が 共通して持たない仮引数 に対する処理を行いたい場合は、ラッパー関数に *args
と **kwargs
の仮引数を記述 します。そうすることで、任意の仮引数を持つ関数 に対する、ラッパー関数を定義 する事ができます。同様の記述 は 以前の記事で任意の関数に対して処理時間を計算する処理を追加するデコレーターの関数 create_show_time
の中で定義された、下記にのプログラムの ラッパー関数 show_time
で既に行っています が、今回との違い は、ラッパー関数が *args
、**kwargs
以外の仮引数を持つ 点です。
1 def create_show_time(func):
2 def show_time(*args, **kwargs):
略
3 return retval
4
5 return show_time
まず、ai_by_score
を下記のプログラムのように修正します。
-
3 行目:ラッパー関数の仮引数に
*args
と**kwargs
を追加する -
4 行目:ラップする関数を呼び出す際に、実引数
*args
と**kwargs
を追加する。*args
の前にdebug=debug
のようなキーワード引数を記述する と、エラーが発生する場合がある ので、debug=debug
を 位置引数debug
に修正 する。詳細はこの後で説明する
1 def ai_by_score(eval_func):
2 @wraps(eval_func)
3 def wrapper(mb_orig, debug=False, rand=True, analyze=False, *args, **kwargs):
元と同じなので省略
4 score = eval_func(mb, debug, *args, **kwargs)
元と同じなので省略
5 return wrapper
行番号のないプログラム
def ai_by_score(eval_func):
@wraps(eval_func)
def wrapper(mb_orig, debug=False, rand=True, analyze=False, *args, **kwargs):
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 = []
if analyze:
score_by_move = {}
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, debug, *args, **kwargs)
dprint(debug, "score", score, "best score", best_score)
if analyze:
score_by_move[move] = 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)
if analyze:
return {
"candidate": best_moves,
"score_by_move": score_by_move,
}
elif rand:
return choice(best_moves)
else:
return best_moves[0]
return wrapper
修正箇所
def ai_by_score(eval_func):
@wraps(eval_func)
- def wrapper(mb_orig, debug=False, rand=True, analyze=False):
+ def wrapper(mb_orig, debug=False, rand=True, analyze=False, *args, **kwargs):
元と同じなので省略
- score = eval_func(mb, debug=debug)
+ score = eval_func(mb, debug, *args, **kwargs)
元と同じなので省略
return wrapper
次に、ai11s
を下記のプログラムのように修正します。
-
2 行目:
score_201
などの、元のai11s
にあった仮引数を追加 する
1 @ai_by_score
2 def ai11s(mb, debug=False, score_201=2, score_102=0.5, score_012=-1):
3 元と同じなので省略
4 return score
行番号のないプログラム
@ai_by_score
def ai11s(mb, debug=False, score_201=2, score_102=0.5, score_012=-1):
# 真ん中のマスに着手している場合は、評価値として 300 を返す
if mb.last_move == (1, 1):
return 300
# 自分が勝利している場合は、評価値として 200 を返す
if mb.status == mb.last_turn:
return 200
markpats = mb.count_markpats()
if debug:
pprint(markpats)
# 相手が勝利できる場合は評価値として -100 を返す
if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
return -100
# 次の自分の手番で自分が必ず勝利できる場合は評価値として 100 を返す
elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
return 100
# 評価値の合計を計算する変数を 0 で初期化する
score = 0
# 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
score += score_201
# 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
# 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
# 計算した評価値を返す
return score
修正箇所
@ai_by_score
-def ai11s(mb, debug=False):
+def ai11s(mb, debug=False, score_201=2, score_102=0.5, score_012=-1):
元と同じなので省略
return score
上記の修正後に下記のプログラムを実行すると、実行結果のようにエラーが発生しなくなったことが確認できます。
print(ai11s(mb))
実行結果
(1, 1)
また、下記のプログラムのように、実引数 debug
、rand
、analyze
を記述 して ai11s
を呼び出しても正しい処理が行われることが確認できます。
print(ai11s(mb, debug=True))
実行結果
長いので略
print(ai11s(mb, rand=False))
実行結果1
(1, 1)
pprint(ai11s(mb, analyze=True))
実行結果2
{'candidate': [(1, 1)],
'score_by_move': {(0, 0): 1.5,
(0, 1): 1.0,
(0, 2): 1.5,
(1, 0): 1.0,
(1, 1): 300,
(1, 2): 1.0,
(2, 0): 1.5,
(2, 1): 1.0,
(2, 2): 1.5}}
*args
の前にキーワード引数を記述した場合に行われる処理
下記の説明は、関数呼び出し を行った際に、実引数 *args
よりも前にキーワード引数を記述 した際に発生する可能性がある エラーについての説明 です。意味がわからない場合は飛ばしてもらってもかまいませんが、その場合でも、実引数 *args
よりも前にキーワード引数を記述しないほうが良い ということだけは覚えて解いてください。
関数呼び出し を行う際に、実引数 *args
の前 に キーワード引数を記述 すると、予期せぬエラーが発生 する場合があります。
具体例を挙げます。下記は、a
、b
、*args
、**kwargs
を仮引数に持つ関数 f
と、仮引数 a
、b
、c
を持つ関数 g
を定義するプログラムです。
f
は、仮引数の値を print
で表示した後で g
を呼び出していますが、その際に *args
よりも前にキーワード引数 b=b
を記述 して呼び出しています。
def f(a, b, *args, **kwargs):
print(a, b, args, kwargs)
g(a, b=b, *args, **kwargs)
def g(a, b, c):
print(a, b, c)
上記の実行後に、下記のプログラムで f(1, 2, 3)
を呼び出すと、実行結果のような エラーが発生 します。
f(1, 2, 3)
実行結果
1 2 (3,) {}
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[14], line 1
----> 1 f(1, 2, 3)
Cell In[13], line 3
1 def f(a, b, *args, **kwargs):
2 print(a, b, args, kwargs)
----> 3 g(a, b=b, *args, **kwargs)
TypeError: g() got multiple values for argument 'b'
エラーメッセージから、g
を呼び出す際 に、仮引数 b
に対して複数(multiple)の値を代入 しようとしてエラーが発生したことがわかります。
f(1, 2, 3)
を実行すると f
の最初で行われる print
の実行結果の 1 2 (3,) {} から、f
の 仮引数 にはそれぞれ 下記の表の値が代入 されることがわかります。
仮引数 | 値 |
---|---|
a |
1 |
b |
2 |
args |
(3, ) という tuple |
kwargs |
{} という 空の dict |
f
の g(a, b=b, *args, **kwargs)
が呼び出されると、以前の記事で説明したように、*args
は 位置引数として展開 されます。また、以前の記事で説明したように、Python では、キーワード引数 は 位置引数の後に記述する 決まりになっているので、位置引数の展開はキーワード引数の前 に行われ、下記のプログラムが実行されます。
g(1, 3, b=2)
その結果、g
の 仮引数 b
には 位置引数の 3
と、キーワード引数 b=2
の 2 種類の値 が 対応する ことになり、その結果 multiple values for argument 'b' というエラーが発生します。
関数 f
を下記のプログラムのように、キーワード引数 b=b
を 位置引数 b
に修正 して定義することで、この エラーは発生しなくなります。
def f(a, b, *args, **kwargs):
print(a, b, args, kwargs)
g(a, b, *args, **kwargs)
修正箇所
def f(a, b, *args, **kwargs):
- print(a, b=b, args, kwargs)
+ print(a, b, args, kwargs)
g(a, b, *args, **kwargs)
上記の修正後に、下記のプログラムで f(1, 2, 3)
を実行すると、f
の g(a, b, *args, **kwargs)
の *args
は、位置引数として b
の後に展開される ので、g(1, 2, 3)
が実行され、実行結果のようにエラーが発生しなくなります。
f(1, 2, 3)
実行結果
1 2 (3,) {}
1 2 3
上記をまとめると以下のようになります。
関数呼び出し の際に実引数に記述した *args
は、位置引数として展開 される。Python では キーワード引数は位置引数の後に記述 するという決まりになっているので、*args
の位置引数の展開 は *args
よりも前に記述したキーワード引数よりも前の位置で展開 される。
その結果、関数呼び出しの際に記述した 実引数の順番 が、*args
を展開した場合 の実引数の 順番と異なってしまう 可能性があり、その結果 エラーが発生してしまう場合が生じる。
従って、*args
の前にキーワード引数を記述しないほうが良い。
*args
よりも前にキーワード引数を記述 した際に、必ずエラーが発生するとは限りません。例えば、下記のプログラムのように f
を定義した場合に、f(1, 2, 3)
を呼び出しても実行結果のようにエラーは発生しません。
def f(a, b, *args, **kwargs):
print(a, b, args, kwargs)
g(a, c=b, *args, **kwargs)
f(1, 2, 3)
修正箇所
def f(a, b, *args, **kwargs):
print(a, b, args, kwargs)
- g(a, b=b, *args, **kwargs)
+ g(a, c=b, *args, **kwargs)
実行結果
1 2 (3,) {}
1 3 2
上記の場合は、g(a, c=b, *args, **kwargs)
は、g(1, 3, c=2)
のように展開されて実行されるので、エラーは発生しません。ただし、エラーが発生しないからといって、上記のような処理を記述することはお勧めしません。
ラッパー関数の仮引数の種類
先程、ラッパー関数の仮引数は以下のように 2 種類に分類できると説明しました。
- ラップする関数を呼び出す際に記述する実引数に関連する仮引数
- ラッパー関数のみで利用する仮引数
実際には、下記の 3 種類に分類 できます。
- 上記の
debug
のように、ラップする関数とラッパー関数の 両方に関連する 仮引数 - 上記以外で、ラップする関数 を呼び出す際に記述する実引数に 関連する 仮引数
- 上記以外で、ラッパー関数のみで利用する 仮引数
また、ラップする関数に関連する 仮引数には、以下のような種類があります
- すべて のラップする関数に 共通する 仮引数
- ラップする関数に 共通しない 仮引数を表す
*args
、**kwargs
ラップする関数とラッパー関数の仮引数の順番の統一
先程、ai11s
を下記のプログラムのように定義しました。
1 @ai_by_score
2 def ai11s(mb, debug=False, score_201=2, score_102=0.5, score_012=-1):
3 元と同じなので省略
4 return score
そのため、ai11s
に対して下記のプログラムのように score_201
など のパラメーターを デフォルト値とは異なる値に設定 して呼び出すことができます。なお、下記のプログラムはパラメーターに異なる値を設定できることを示すための例なので、実行結果は変わりません。
print(ai11s(mb, debug=False, score_201=3, score_102=2))
実行結果
(1, 1)
しかし、上記と同じ処理を、キーワード引数を使わずに下記のプログラムのように 位置引数を記述して実行 すると、実行結果のように そのような記述を行っていない にも関わらず、実引数に analyze=True
が記述された場合の処理 が行われます。
pprint(ai11s(mb, False, 3, 2))
実行結果
{'candidate': [(1, 1)],
'score_by_move': {(0, 0): 1.5,
(0, 1): 1.0,
(0, 2): 1.5,
(1, 0): 1.0,
(1, 1): 300,
(1, 2): 1.0,
(2, 0): 1.5,
(2, 1): 1.0,
(2, 2): 1.5}}
このようなことが起きる理由は、上記を実行すると、下記の 3 行目で定義された ラッパー関数が呼び出される ためです。
1 def ai_by_score(eval_func):
2 @wraps(eval_func)
3 def wrapper(mb_orig, debug=False, rand=True, analyze=False, *args, **kwargs):
元と同じなので省略
4 score = eval_func(mb, debug, *args, **kwargs)
元と同じなので省略
5 return wrapper
下記の表はそれぞれの 位置引数 がラッパー関数の どの仮引数に代入されるか を表します。
位置引数の値 | 代入される仮引数 |
---|---|
mb |
mb_orig |
False |
debug |
3 |
rand |
2 |
analyze |
上記の表のように仮引数 analyze
には 2
が代入 されますが、Python では、条件式 の計算結果が bool 型以外 の場合は、以下の値を False
、それ以外の値を True
とみなします。
False
、None
, 数値の 0
、空文字の ""
、要素が存在しない list や tuple など コンテナデータ型 のデータ。詳細は下記のリンク先を参照して下さい。
従って、analyze
に 2
が代入 されている場合は、上記の実行結果のように、analyze=True
を記述 して ai11s
を呼び出した場合と 同じ処理 が行われます。
このようなことが起きる理由は、ラップする関数 の 仮引数の順番 と、ラッパー関数 の 仮引数の順番 が、下記のプログラムのように 異なる からです。
def ai11s(mb, debug=False, score_201=2, score_102=0.5, score_012=-1):
def wrapper(mb_orig, debug=False, rand=True, analyze=False, *args, **kwargs):
キーワード引数専用の仮引数
この問題を解決する方法の一つに、キーワード引数専用の仮引数を記述する という方法があります。具体的には、*args より後3に記述 された仮引数は、キーワード引数専用の仮引数 となり、位置引数で記述された実引数の値が代入されることはなくなります。
具体例を挙げます。下記は、a
、b
、*args
、c
の順で 4 つの 仮引数 を持つ 関数 f
の定義 で、それぞれの仮引数を print
で表示する処理を行います。
def f(a, b=2, *args, c=3):
print(a, b, c, args)
下記のプログラムを実行すると、位置引数 5
、6
はそれぞれ仮引数 a
、b
に代入されますが、仮引数 c
は *args
より後で記述 されているので、4 つ目の 位置引数である 8
は 仮引数 c
には代入されません。その結果、位置引数 7
、8
に 直接対応する仮引数が存在しない ため args
には実行結果のように (7, 8)
が代入 され、c
にはデフォルト値である 3
が代入 されます。
f(5, 6, 7, 8)
実行結果
5 6 3 (7, 8)
仮引数 c
に値を代入 するためには、下記のプログラムのように、キーワード引数を記述する必要 があります。実行結果のように、キーワード引数 c=8
を記述 する事で 仮引数 c
に 8
が代入 され、args
には (7, )
という tuple が代入されます。
f(5, 6, 7, c=8)
実行結果
5 6 8 (7,)
ai_by_score
の修正
そこで、下記のプログラムの 3 行目のように、ラッパー関数のみで利用する仮引数 を、*args
の後ろに記述 するように ai_by_score
を修正 します。ラッパー関数とラップする関数の 両方で利用する仮引数 debug
は、ラップする関数 の 仮引数と同じ位置に記述 します。
1 def ai_by_score(eval_func):
2 @wraps(eval_func)
3 def wrapper(mb_orig, debug=False, *args, rand=True, analyze=False, **kwargs):
元と同じなので省略
4
5 return wrapper
行番号のないプログラム
def ai_by_score(eval_func):
@wraps(eval_func)
def wrapper(mb_orig, debug=False, *args, rand=True, analyze=False, **kwargs):
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 = []
if analyze:
score_by_move = {}
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, debug, *args, **kwargs)
dprint(debug, "score", score, "best score", best_score)
if analyze:
score_by_move[move] = 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)
if analyze:
return {
"candidate": best_moves,
"score_by_move": score_by_move,
}
elif rand:
return choice(best_moves)
else:
return best_moves[0]
return wrapper
修正箇所
def ai_by_score(eval_func):
@wraps(eval_func)
- def wrapper(mb_orig, debug=False, rand=True, analyze=False, *args, **kwargs):
+ def wrapper(mb_orig, debug=False, *args, rand=True, analyze=False, **kwargs):
元と同じなので省略
return wrapper
ラッパー関数の仮引数 を以下のように定義する事で、ラップする関数の仮引数と同じ順番 で、ラッパー関数の実引数を記述できる ようになります。なお、この修正によって、ラッパー関数のみで利用する仮引数 を、必ずキーワード引数で記述する必要が生じる 点に注意して下さい。
- ラップする関数で共通する仮引数 を、ラッパー関数の 先頭にラップする関数と同じ順番 で定義する
-
その後 に、ラップする関数で共通しない仮引数を代入するための 仮引数
*args
を定義 する -
ラッパー関数のみで利用 する仮引数を、
*args
の後ろに定義 する
ai_by_score
を修正したので、ai11s
を下記のプログラムで 定義し直す必要 があります。なお、ai11s
の定義そのものには変更は全くありません。
@ai_by_score
def ai11s(mb, score_201=2, score_102=0.5, score_012=-1, debug=False):
元と同じなので省略
return score
行番号のないプログラム
@ai_by_score
def ai11s(mb, score_201=2, score_102=0.5, score_012=-1, debug=False):
# 真ん中のマスに着手している場合は、評価値として 300 を返す
if mb.last_move == (1, 1):
return 300
# 自分が勝利している場合は、評価値として 200 を返す
if mb.status == mb.last_turn:
return 200
markpats = mb.count_markpats()
if debug:
pprint(markpats)
# 相手が勝利できる場合は評価値として -100 を返す
if markpats[Markpat(last_turn=0, turn=2, empty=1)] > 0:
return -100
# 次の自分の手番で自分が必ず勝利できる場合は評価値として 100 を返す
elif markpats[Markpat(last_turn=2, turn=0, empty=1)] >= 2:
return 100
# 評価値の合計を計算する変数を 0 で初期化する
score = 0
# 次の自分の手番で自分が勝利できる場合は評価値に score_201 を加算する
if markpats[Markpat(last_turn=2, turn=0, empty=1)] == 1:
score += score_201
# 「自 1 敵 0 空 2」1 つあたり score_102 だけ、評価値を加算する
score += markpats[Markpat(last_turn=1, turn=0, empty=2)] * score_102
# 「自 0 敵 1 空 2」1 つあたり score_201 だけ、評価値を減算する
score += markpats[Markpat(last_turn=0, turn=1, empty=2)] * score_012
# 計算した評価値を返す
return score
上記の修正後に下記のプログラムを実行すると、意図通りの処理が行われるようになったことが確認できます。
pprint(ai11s(mb, False, 3, 2))
実行結果
(1, 1)
上記ではすべての実引数を位置引数で記述して ai11s
を呼び出すことができるようにしましたが、位置引数が多いとプログラムの意味がわかりづらくなる ので、実際に ai11s
を利用する場合は、最初の mb
に対応する実引数以外 は、キーワード引数で記述したほうが良い でしょう。
キーワード引数専用の仮引数の詳細については、下記のリンク先を参照して下さい。
今回の記事では利用しませんが、位置引数専用の仮引数 を定義する事もできます。具体的には、下記のプログラムのように /
の前に記述された仮引数 は、位置引数で記述された実引数の値のみが代入されます。
def f(a, b=1, /, c=2, **kwargs):
print(a, b, c, kwargs)
下記のプログラムを実行すると、以下のような処理が行われます。
- 仮引数
b
は/
より前に記述された位置引数専用の仮引数なので、キーワード引数b=6
の値はb
には代入されない -
b=6
の値は、それを代入する仮引数が存在しないのでkwargs
の方に代入される - キーワード引数
c=7
の値は仮引数c
に代入される
f(5, b=6, c=7)
実行結果
5 1 7 {'b': 6}
位置引数専用の仮引数の詳細については、下記のリンク先を参照して下さい。
今回の記事のまとめ
今回の記事では、ラッパー関数とラップする関数の仮引数の関係について説明し、評価値を計算する際に、mb
以外のパラメーターを必要とする ai11s
をデコレータ式で定義しなおしました。
実は、今回の記事の修正によって、ai2s
が正しく動作しなくなっているので、次回の記事では ai2s
も含めた AI の関数が正しく動作するようにデコレーター式を使って定義し直すことにします。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
ai.py | 本記事で更新した ai_new.py |
次回の記事