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

# 目次と前回の記事

https://qiita.com/ysgeso/items/2381dd4e3283cbed49a0

https://qiita.com/ysgeso/items/7e1d948c4b9208ab44fb

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

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

| リンク | 説明 |
|:--|:--|
| [marubatsu.py](https://github.com/ysgeso/marubatsu/blob/master/131/marubatsu.py)  | Marubatsu、Marubatsu_GUI クラスの定義|
| [ai.py](https://github.com/ysgeso/marubatsu/blob/master/131/ai.py)  |  AI に関する関数 |
| [test.py](https://github.com/ysgeso/marubatsu/blob/master/131/test.py)  |  テストに関する関数 |
|  [util.py](https://github.com/ysgeso/marubatsu/blob/master/131/util.py) |  ユーティリティ関数の定義。現在は `gui_play` のみ定義されている |
| [tree.py](https://github.com/ysgeso/marubatsu/blob/master/131/tree.py)  | ゲーム木に関する Node、Mbtree クラスの定義  |
| [gui.py](https://github.com/ysgeso/marubatsu/blob/master/131/gui.py)  |  GUI に関する処理を行う基底クラスとなる GUI クラスの定義  |

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

https://qiita.com/ysgeso/items/10d1d01192c014173e4b

# ラッパー関数とラップする関数の仮引数

前回の記事では、__`ai_by_score` をデコレーター式に記述__ して、__局面の評価値を計算して着手を選択する AI を定義__ しました。`ai_by_score` が定義した __ラップする関数__ は以下のような性質を持ちます。

* __仮引数 `mb`__ に評価値を計算する __局面を表すデータを代入__ する
* __`mb` の局面の評価値__ を計算して __返り値として返す__

下記は[前回の記事](https://qiita.com/ysgeso/items/7e1d948c4b9208ab44fb#%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF%E3%83%BC%E5%BC%8F%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F-ai2s-%E3%81%AE%E5%AE%9A%E7%BE%A9)でデコレーター式に `@ai_by_score` を記述して定義した __`ai2s`__ のプログラムです。

```python
@ai_by_score
def ai2s(mb):
    return 0
```

また、デコレーターが定義する __ラッパー関数__ は以下のような性質を持ちます。

* __ラップする関数を呼び出す際に必要__ となる `mb` を計算するために必要となる __仮引数 `mb_orig` を持つ__
* 仮引数として、__ラッパー関数のみで利用__ する `debug`、`rand`、`analyze` を持つ

下記は、デコレーターの関数 __[`ai_by_score`](https://qiita.com/ysgeso/items/7e1d948c4b9208ab44fb#%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF%E3%81%A8%E3%81%97%E3%81%A6%E3%81%AE-ai_by_score-%E3%81%AE%E5%AE%9A%E7%BE%A9)__ の定義で、__ラッパー関数の定義は 8 行目__ で行われています。また、ラッパー関数の中では 11 ~ 13 行目の処理で __`mb_orig` から__ ラップする関数を呼び出す際に実引数に記述する __`mb` を作成__ して __ラップする関数を呼び出しています__。

```python
 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
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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
```
</details>

上記の __ラッパー関数の仮引数__ は以下のように __2 種類に分類__ できます。

* __ラップする関数__ を呼び出す際に記述する実引数に __関連する__ 仮引数 `mb_orig`
* __ラッパー関数のみで利用__ する仮引数 `debug`、`rand`、`analyze`

## `mb` 以外の仮引数を持つ AI の関数のラッパー関数

評価値を計算することで着手を選択する AI の関数には、下記のプログラムの `ai11s` のように、__`mb` 以外を仮引数として持つ__ ものがあります。このような関数に対しては、上記の __`ai_by_score`__ をデコレーターとして __利用することはできません__。

```python
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)
```

下記のプログラムで、[前回の記事](https://qiita.com/ysgeso/items/7e1d948c4b9208ab44fb#%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF%E3%83%BC%E5%BC%8F%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F-ai2s-%E3%81%AE%E5%AE%9A%E7%BE%A9)でデコレーター式を使って `ai2s` を定義しなおしたのと同様の方法で `ai11s` を定義しなおしても、この時点ではエラーは発生しません。

```python
from marubatsu import Marubatsu, Markpat
from pprint import pprint

@ai_by_score
def ai11s(mb):    
元の ai11s の中の eval_func と同じなので略
    return score
```

<details><summary><strong>プログラム全体</strong></summary>

```python
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


```
</details>

しかし、上記の実行後に下記のプログラムで、ゲーム開始時の局面に対して __`ai11s` で着手を計算__ しようとすると、実行結果のように __エラーが発生__ します。このエラーの原因について少し考えてみて下さい。

```python
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` の [クロージャー変数](https://qiita.com/ysgeso/items/18ad5c20101c96aaecae#%E3%82%AF%E3%83%AD%E3%83%BC%E3%82%B8%E3%83%A3%E3%83%BC%E5%A4%89%E6%95%B0)__ として __利用できる__ のでエラーは発生しません。

```python
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` が呼び出されます__。

```python
 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` が定義されていないというエラーが発生します。

```python
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` を記述する

```python
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
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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
```
</details>

次に、__`ai11s`__ を下記のプログラムのように修正します。

* __2 行目__：仮引数 `debug` を追加する。元の `ai11s` の定義に倣って、デフォルト値を `False` とするデフォルト引数とした

```python
1  @ai_by_score
2  def ai11s(mb, debug=False):
元と同じなので省略
3      return score
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
@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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
@ai_by_score
-def ai11s(mb):      
+def ai11s(mb, debug=False):      
元と同じなので省略
    return score
```
</details>

:::note
__`ai11s` を定義する際__ にデコレーター式 __`@ai_by_score` を利用__ しているので、__`ai11s` よりも前__ に、__`ai_by_score` の修正を行う必要がある__ 点に注意して下さい。
:::

上記の修正後に下記のプログラムを実行すると、実行結果のような __別のエラーが発生__ します。これは、先程の `debug` と同様に __`score_102` が定義されていない__ ことが原因です。

```python
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` の仮引数を記述__ します。そうすることで、__任意の仮引数を持つ関数__ に対する、__ラッパー関数を定義__ する事ができます。__同様の記述__ は [以前の記事](https://qiita.com/ysgeso/items/18ad5c20101c96aaecae#%E3%83%A9%E3%83%83%E3%83%91%E3%83%BC%E9%96%A2%E6%95%B0%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B%E9%96%A2%E6%95%B0)で任意の関数に対して処理時間を計算する処理を追加するデコレーターの関数 __`create_show_time`__ の中で定義された、下記にのプログラムの __ラッパー関数 `show_time` で既に行っています__ が、__今回との違い__ は、ラッパー関数が __`*args`、`**kwargs` 以外の仮引数を持つ__ 点です。

```python
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` に修正__ する。詳細はこの後で説明する

```python
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
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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
```
</details>

次に、__`ai11s`__ を下記のプログラムのように修正します。

* __2 行目__：`score_201` などの、__元の `ai11s` にあった仮引数を追加__ する

```python
1  @ai_by_score
2  def ai11s(mb, debug=False, score_201=2, score_102=0.5, score_012=-1):      
3  元と同じなので省略
4      return score
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
@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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
@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
```
</details>

上記の修正後に下記のプログラムを実行すると、実行結果のようにエラーが発生しなくなったことが確認できます。

```python
print(ai11s(mb))
```

実行結果

```
(1, 1)
```

また、下記のプログラムのように、__実引数 `debug`、`rand`、`analyze` を記述__ して `ai11s` を呼び出しても正しい処理が行われることが確認できます。

```python
print(ai11s(mb, debug=True))
```

実行結果

```
長いので略
```

```python
print(ai11s(mb, rand=False))
```

実行結果[^1]

```
(1, 1)
```

```python
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` を記述__ して呼び出しています。

```python
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)`__ を呼び出すと、実行結果のような __エラーが発生__ します。

```python
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)` が呼び出されると、[以前の記事](https://qiita.com/ysgeso/items/474db9ea1805993eaa34#%E5%8F%8D%E5%BE%A9%E5%8F%AF%E8%83%BD%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%B1%95%E9%96%8B)で説明したように、`*args` は __位置引数として展開__ されます。また、[以前の記事](https://qiita.com/ysgeso/items/c5dbe62b329f7936181a#%E4%BD%8D%E7%BD%AE%E5%BC%95%E6%95%B0%E3%81%A8%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89%E5%BC%95%E6%95%B0%E3%81%AE%E4%B8%A1%E6%96%B9%E3%82%92%E4%BD%BF%E3%81%86%E5%A0%B4%E5%90%88)で説明したように、Python では、__キーワード引数__ は __位置引数の後に記述する__ 決まりになっているので、__位置引数の展開はキーワード引数の前__ に行われ、下記のプログラムが実行されます。

```python
g(1, 3, b=2)
```

その結果、`g` の __仮引数 `b`__ には __位置引数の `3`__ と、__キーワード引数 `b=2`__ の __2 種類の値__ が __対応する__ ことになり、その結果 multiple values for argument 'b' というエラーが発生します。

関数 `f` を下記のプログラムのように、キーワード引数 `b=b` を __位置引数 `b` に修正__ して定義することで、この __エラーは発生しなくなります__。

```python
def f(a, b, *args, **kwargs):
    print(a, b, args, kwargs)
    g(a, b, *args, **kwargs)
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def f(a, b, *args, **kwargs):
-   print(a, b=b, args, kwargs)
+   print(a, b, args, kwargs)
    g(a, b, *args, **kwargs)
```
</details>

上記の修正後に、下記のプログラムで `f(1, 2, 3)` を実行すると、`f` の `g(a, b, *args, **kwargs)` の __`*args`__ は、位置引数として __`b` の後に展開される__ ので、`g(1, 2, 3)` が実行され、実行結果のようにエラーが発生しなくなります。

```python
f(1, 2, 3)
```

実行結果

```
1 2 (3,) {}
1 2 3
```

上記をまとめると以下のようになります。

:::note warn
__関数呼び出し__ の際に実引数に記述した __`*args`__ は、__位置引数として展開__ される。Python では __キーワード引数は位置引数の後に記述__ するという決まりになっているので、__`*args` の位置引数の展開__ は __`*args` よりも前に記述したキーワード引数よりも前の位置で展開__ される。

その結果、関数呼び出しの際に記述した __実引数の順番__ が、__`*args` を展開した場合__ の実引数の __順番と異なってしまう__ 可能性があり、その結果 __エラーが発生してしまう場合が生じる__。

従って、__`*args` の前にキーワード引数を記述しないほうが良い__。
:::

:::note
__`*args` よりも前にキーワード引数を記述__ した際に、__必ずエラーが発生するとは限りません__。例えば、下記のプログラムのように `f` を定義した場合に、`f(1, 2, 3)` を呼び出しても実行結果のようにエラーは発生しません。

```python
def f(a, b, *args, **kwargs):
    print(a, b, args, kwargs)
    g(a, c=b, *args, **kwargs)

f(1, 2, 3)
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def f(a, b, *args, **kwargs):
    print(a, b, args, kwargs)
-   g(a, b=b, *args, **kwargs)
+   g(a, c=b, *args, **kwargs)
```
</details>

実行結果

```
1 2 (3,) {}
1 3 2
```

上記の場合は、`g(a, c=b, *args, **kwargs)` は、`g(1, 3, c=2)` のように展開されて実行されるので、エラーは発生しません。ただし、エラーが発生しないからといって、上記のような処理を記述することはお勧めしません。
:::

## ラッパー関数の仮引数の種類

先程、ラッパー関数の仮引数は以下のように 2 種類に分類できると説明しました。

* ラップする関数を呼び出す際に記述する実引数に関連する仮引数
* ラッパー関数のみで利用する仮引数

__実際には__、下記の __3 種類に分類__ できます。

* 上記の __`debug`__ のように、ラップする関数とラッパー関数の __両方に関連する__ 仮引数
* 上記以外で、__ラップする関数__ を呼び出す際に記述する実引数に __関連する__ 仮引数
* 上記以外で、__ラッパー関数のみで利用する__ 仮引数

また、__ラップする関数に関連する__ 仮引数には、以下のような種類があります

* __すべて__ のラップする関数に __共通する__ 仮引数
* ラップする関数に __共通しない__ 仮引数を表す __`*args`、`**kwargs`__

## ラップする関数とラッパー関数の仮引数の順番の統一

先程、__`ai11s`__ を下記のプログラムのように定義しました。

```python
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` など__ のパラメーターを __デフォルト値とは異なる値に設定__ して呼び出すことができます。なお、下記のプログラムはパラメーターに異なる値を設定できることを示すための例なので、実行結果は変わりません。

```python
print(ai11s(mb, debug=False, score_201=3, score_102=2))
```

実行結果

```
(1, 1)
```

しかし、上記と同じ処理を、キーワード引数を使わずに下記のプログラムのように __位置引数を記述して実行__ すると、実行結果のように __そのような記述を行っていない__ にも関わらず、__実引数に `analyze=True` が記述された場合の処理__ が行われます。

```python
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 行目で定義された __ラッパー関数が呼び出される__ ためです。

```python
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 など __[コンテナデータ型](https://docs.python.org/ja/3/library/collections.html)__ のデータ。詳細は下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/reference/expressions.html#boolean-operations

従って、__`analyze` に `2` が代入__ されている場合は、上記の実行結果のように、__`analyze=True` を記述__ して `ai11s` を呼び出した場合と __同じ処理__ が行われます。

このようなことが起きる理由は、__ラップする関数__ の __仮引数の順番__ と、__ラッパー関数__ の __仮引数の順番__ が、下記のプログラムのように __異なる__ からです。

```python
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` で表示する処理を行います。

```python
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` が代入__ されます。

```python
f(5, 6, 7, 8)
```

実行結果

```
5 6 3 (7, 8)
```

__仮引数 `c` に値を代入__ するためには、下記のプログラムのように、__キーワード引数を記述する必要__ があります。実行結果のように、__キーワード引数 `c=8` を記述__ する事で __仮引数 `c` に `8` が代入__ され、`args` には `(7, )` という tuple が代入されます。

```python
f(5, 6, 7, c=8)
```

実行結果

```
5 6 8 (7,)
```

### `ai_by_score` の修正

そこで、下記のプログラムの 3 行目のように、__ラッパー関数のみで利用する仮引数__ を、__`*args` の後ろに記述__ するように __`ai_by_score` を修正__ します。ラッパー関数とラップする関数の __両方で利用する仮引数 `debug`__ は、__ラップする関数__ の __仮引数と同じ位置に記述__ します。

```python
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
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
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
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
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
```
</details>

:::note warn
__ラッパー関数の仮引数__ を以下のように定義する事で、__ラップする関数の仮引数と同じ順番__ で、__ラッパー関数の実引数を記述できる__ ようになります。なお、この修正によって、__ラッパー関数のみで利用する仮引数__ を、__必ずキーワード引数で記述する必要が生じる__ 点に注意して下さい。

* __ラップする関数で共通する仮引数__ を、ラッパー関数の __先頭にラップする関数と同じ順番__ で定義する
* __その後__ に、ラップする関数で共通しない仮引数を代入するための __仮引数 `*args` を定義__ する
* __ラッパー関数のみで利用__ する仮引数を、__`*args` の後ろに定義__ する
:::

`ai_by_score` を修正したので、__`ai11s`__ を下記のプログラムで __定義し直す必要__ があります。なお、`ai11s` の定義そのものには変更は全くありません。

```python
@ai_by_score
def ai11s(mb, score_201=2, score_102=0.5, score_012=-1, debug=False):      
元と同じなので省略
    return score
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
@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
```
</details>

上記の修正後に下記のプログラムを実行すると、意図通りの処理が行われるようになったことが確認できます。

```python
pprint(ai11s(mb, False, 3, 2))
```

実行結果

```
(1, 1)
```

:::note
上記ではすべての実引数を位置引数で記述して `ai11s` を呼び出すことができるようにしましたが、__位置引数が多いとプログラムの意味がわかりづらくなる__ ので、実際に `ai11s` を利用する場合は、__最初の `mb` に対応する実引数以外__ は、__キーワード引数で記述したほうが良い__ でしょう。
:::

キーワード引数専用の仮引数の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/reference/compound_stmts.html#function-definitions

:::note
今回の記事では利用しませんが、__位置引数専用の仮引数__ を定義する事もできます。具体的には、下記のプログラムのように __`/` の前に記述された仮引数__ は、位置引数で記述された実引数の値のみが代入されます。

```python
def f(a, b=1, /, c=2, **kwargs):
    print(a, b, c, kwargs)
```

下記のプログラムを実行すると、以下のような処理が行われます。

* 仮引数 `b` は `/` より前に記述された位置引数専用の仮引数なので、キーワード引数 `b=6` の値は `b` には代入されない
* `b=6` の値は、それを代入する仮引数が存在しないので `kwargs` の方に代入される
* キーワード引数 `c=7` の値は仮引数 `c` に代入される

```python
f(5, b=6, c=7)
```

実行結果

```
5 1 7 {'b': 6}
```

位置引数専用の仮引数の詳細については、下記のリンク先を参照して下さい。
:::

https://docs.python.org/ja/3/reference/compound_stmts.html#function-definitions

# 今回の記事のまとめ

今回の記事では、ラッパー関数とラップする関数の仮引数の関係について説明し、評価値を計算する際に、`mb` 以外のパラメーターを必要とする `ai11s` をデコレータ式で定義しなおしました。

実は、今回の記事の修正によって、`ai2s` が正しく動作しなくなっているので、次回の記事では `ai2s` も含めた AI の関数が正しく動作するようにデコレーター式を使って定義し直すことにします。

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

| リンク | 説明 |
|:--|:--|
| [marubatsu.ipynb](https://github.com/ysgeso/marubatsu/blob/master/131/marubatsu.ipynb)  | 本記事で入力して実行した JupyterLab のファイル |
| [ai.py](https://github.com/ysgeso/marubatsu/blob/master/131/ai_new.py)  | 本記事で更新した ai_new.py  |

# 次回の記事

https://qiita.com/ysgeso/items/b1cfaa9c4ec963d27820

[^1]:`ai11s` はゲーム開始時の局面で (1, 1) にしか着手しないので、`rand=False` を記述しない場合と実行結果は変わりません
[^2]:(1, 1) が選択される理由が、評価値の表示から明確になります
[^3]:正確には、`*` で始まる仮引数であれば、`*args` でなくてもかまいませんが、`*` で始まる仮引数の名前は `*args` とするのが一般的です