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?

Pythonで〇×ゲームのAIを一から作成する その131 ラッパー関数とラップする関数の仮引数の関係

Last updated at Posted at 2024-11-09

目次と前回の記事

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

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

リンク 説明
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 を持つ
  • 仮引数として、ラッパー関数のみで利用 する debugrandanalyze を持つ

下記は、デコレーターの関数 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
  • ラッパー関数のみで利用 する仮引数 debugrandanalyze

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_scoreai11s の修正

このように、デコレーター式でラップする関数 はラッパー関数の ローカル関数として定義されていない ので、その中の処理で ラッパー関数のローカル変数 をクロージャー変数として 利用することはできません。そのため、ラッパー関数のローカル変数ラップする関数で利用する ためには、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)

また、下記のプログラムのように、実引数 debugrandanalyze を記述 して 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 の前キーワード引数を記述 すると、予期せぬエラーが発生 する場合があります。

具体例を挙げます。下記は、ab*args**kwargs を仮引数に持つ関数 f と、仮引数 abc を持つ関数 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

fg(a, b=b, *args, **kwargs) が呼び出されると、以前の記事で説明したように、*args位置引数として展開 されます。また、以前の記事で説明したように、Python では、キーワード引数位置引数の後に記述する 決まりになっているので、位置引数の展開はキーワード引数の前 に行われ、下記のプログラムが実行されます。

g(1, 3, b=2)

その結果、g仮引数 b には 位置引数の 3 と、キーワード引数 b=22 種類の値対応する ことになり、その結果 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) を実行すると、fg(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 とみなします

FalseNone, 数値の 0空文字の ""要素が存在しない list や tuple など コンテナデータ型 のデータ。詳細は下記のリンク先を参照して下さい。

従って、analyze2 が代入 されている場合は、上記の実行結果のように、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に記述 された仮引数は、キーワード引数専用の仮引数 となり、位置引数で記述された実引数の値が代入されることはなくなります。

具体例を挙げます。下記は、ab*argsc の順で 4 つの 仮引数 を持つ 関数 f の定義 で、それぞれの仮引数を print で表示する処理を行います。

def f(a, b=2, *args, c=3):
    print(a, b, c, args)

下記のプログラムを実行すると、位置引数 56 はそれぞれ仮引数 ab に代入されますが、仮引数 c*args より後で記述 されているので、4 つ目の 位置引数である 8仮引数 c には代入されません。その結果、位置引数 78直接対応する仮引数が存在しない ため args には実行結果のように (7, 8) が代入 され、c にはデフォルト値である 3 が代入 されます。

f(5, 6, 7, 8)

実行結果

5 6 3 (7, 8)

仮引数 c に値を代入 するためには、下記のプログラムのように、キーワード引数を記述する必要 があります。実行結果のように、キーワード引数 c=8 を記述 する事で 仮引数 c8 が代入 され、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

次回の記事

  1. ai11s はゲーム開始時の局面で (1, 1) にしか着手しないので、rand=False を記述しない場合と実行結果は変わりません

  2. (1, 1) が選択される理由が、評価値の表示から明確になります

  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?