Pythonで〇×ゲームのAIを一から作成する　その127　ラッパー関数

# 目次と前回の記事

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

https://qiita.com/ysgeso/items/2c76474ff139a824f4fd

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

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

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

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

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

# 今回の記事の内容

前回の記事では Marubatsu_GUI クラスを、「ゲーム盤のマスに、そのマスに着手を行った際の AI の評価値を表示できる」ように改良するために、[`ai3`](https://qiita.com/ysgeso/items/2c76474ff139a824f4fd#ai3-%E3%81%AE%E4%BF%AE%E6%AD%A3) と [`ai3s`](https://qiita.com/ysgeso/items/2c76474ff139a824f4fd#ai3s-%E3%81%AE%E4%BF%AE%E6%AD%A3) を下記のように修正しました。

* 仮引数 `analyze` が `False` の場合は AI が計算した着手を返すようにする
* 仮引数 `analyze` が `True` の場合は「その AI が最善手とみなした候補手の一覧」と「それぞれの合法手を着手した局面の評価値」を表すデータを返すようにする

残りの AI の関数に対しても同様の修正を行う必要がありますが、__AI の関数の数が多い__ ためそのような修正を行うことは __かなり大変__ です。__複数の AI の関数__ に対する上記のような __同じ機能の拡張__ は、Python の __デコレータ__ を利用することで簡潔に行うことができます。

ただし、デコレータは、実際には __それほど複雑なものではない__ のですが、一見すると __かなり複雑に見える__ ものなので、今回の記事ではデコレータを __理解するために必要__ となる __ラッパー関数__ について説明し、次回の記事でデコレータを使って AI の関数を修正する方法を紹介することにします。

# 関数の機能を拡張する方法

__関数の機能の拡張__ には、主に __関数の定義を修正__ して拡張する方法と、__ラッパー関数を定義__ して拡張する方法があります。

具体例として、下記の関数に対して __処理時間を計算して表示する__ という __機能を追加する方法__ を紹介します。なお、関数の名前や行う処理は __何でも構わない__ ので、下記の関数では __関数の名前を `f`__、行う処理を __`print` でメッセージを表示する__ としました。なお、関数 `f` は仮引数持たず、返り値を返さない関数ですが、仮引数を持ったり、返り値を返す関数を拡張する方法についてはこの後で紹介します。


```python
def f():
    print("関数 f を実行しました") 
```

関数 `f` を呼び出すと、実行結果のようにメッセージが表示されます。

```python
f()
```

実行結果

```
関数 f を実行しました
```

## 関数の定義を修正する方法

簡単に思いつく方法として、下記のプログラムのように __`f` の定義の中__ に処理時間を計算して表示する __処理を追加する__ という、__関数の定義を修正する__ 方法があります。

__関数の処理時間__ は、[以前の記事](https://qiita.com/ysgeso/items/5941cc59c3e0e173c5cf#datetime-%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AB%E3%82%88%E3%82%8B%E7%8F%BE%E5%9C%A8%E6%99%82%E5%88%BB%E3%81%AE%E5%8F%96%E5%BE%97)で説明した、__datetime__ モジュールの __`datetime.now`__ という __現在時刻を計算する関数__ を利用することで計算できます。具体的には、__`datetime.now()`__ で計算した現在時刻のデータを __`-` 演算子で演算する__ ことで、__2 つの時刻の差__ を計算することができます。

* __4 行目__：関数 `f` のブロックの先頭でその時点の時刻を計算し、`starttime` に代入する
* __6、7 行目__：関数 `f` の末尾で、その時点の時刻を計算して `endtime` に代入し、`starttime` との差を計算して表示することで、処理時間を表示する

```python
1  from datetime import datetime
2
3  def f():
4      starttime = datetime.now()
5      print("関数 f を実行しました")
6      endtime = datetime.now()
7      print(endtime - starttime)
```

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

```python
from datetime import datetime

def f():
    starttime = datetime.now()
    print("関数 f を実行しました")
    endtime = datetime.now()
    print(endtime - starttime)
```
</details>

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

```diff_python
from datetime import datetime

def f():
+   starttime = datetime.now()
    print("関数 f を実行しました")
+   endtime = datetime.now()
+   print(endtime - starttime)
```
</details>

:::note
上記では行っていませんが、4 行目の後に `print(starttime)` を、6 行目の後に `print(endtime)` 記述することで、関数の処理を開始した時刻と終了した時刻を表示するという工夫が考えられます。
:::

上記の修正後に下記のプログラムを実行することで、実行結果のように処理時間が表示されるようになります。なお、`print("関数 f を実行しました")` の __処理時間が短すぎる__ ため、筆者のパソコンでは実行結果に __0 時間 0 分 0 秒を表す `0:00:00` が表示__ されました。コンピュータの性能によって表示される処理時間は変化します。

```python
f()
```

実行結果

```
関数 f を実行しました
0:00:00
```

処理時間が計算されることを確認する方法として、関数 `f` に下記のプログラムの 3 ~ 5 行目のように、特に意味はありませんが、__時間がかかる処理を加えて__ `f()` を実行すると、実行結果のように、処理に 0.001002 秒かかったことが表示されます。

```python
1  def f():
2      starttime = datetime.now()
3      c = 0
4      for i in range(10000):
5          c += i
6      print("関数 f を実行しました")
7      endtime = datetime.now()
8    print(endtime - starttime)
```

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

```python
def f():
    starttime = datetime.now()
    c = 0
    for i in range(10000):
        c += i
    print("関数 f を実行しました")
    endtime = datetime.now()
    print(endtime - starttime)
```
</details>

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

```diff_python
def f():
    starttime = datetime.now()
+   c = 0
+   for i in range(10000):
+       c += i
    print("関数 f を実行しました")
    endtime = datetime.now()
    print(endtime - starttime)
```
</details>

```python
f()
```

実行結果（コンピューターの性能によって表示が異なる場合があります）

```
関数 f を実行しました
0:00:00.001002
```

### この方法の欠点

__一般的__ には、関数の機能の拡張や修正は __関数の定義を修正する方法で行います__ が、この方法で __同じような機能の拡張__ を __複数の関数に対して行う場合__ は、それぞれの関数に対して __同様の修正を行う必要があるのが大変__ という __欠点__ があります。

例えば、下記のプログラムの 2、3 行目で定義されている関数 `g` に対して同様の機能の拡張を行おうと思った場合は、6 ~ 10 行目のように `g` を修正する必要があり、このような修正を、__機能を拡張したい関数ごとに行う__ 必要があります。

```python
 1  # 元の g の定義
 2  def g():
 3      print("関数 g を実行しました")
 4
 5  # 拡張後の g の定義
 6  def g():
 7      starttime = datetime.now()
 8      print("関数 g を実行しました")
 9      endtime = datetime.now()
10      print(endtime - starttime)
```

逆に言えば、複数の関数に対して __同じような機能の拡張を行う必要がない__ 場合は、__関数の定義を修正する方法__ で行ったほうが、__余計な手間がかかりません__。従って、この後で説明するラッパー関数による方法は、常に利用すればよいというものではありません。

## ラッパー関数による関数の機能の拡張

関数の機能を拡張する別の方法として下記のような方法があります。

* __その関数を呼び出す処理__ を行う __ラッパー関数__ と呼ばれる __別の関数を定義する__
* ラッパー関数では、関数を呼び出す処理の __前後に機能を拡張する処理を記述__ することで、関数の機能の拡張を行う

関数呼び出しの処理を別の関数の定義の中で記述することが、__関数を包み込む（wrap）ように見える__ ことから、そのような関数のことを __ラッパー（wrapper）関数__ と呼びます。

### ラッパー関数の記述方法

上記の関数 `f` の機能をラッパー関数で拡張する方法を説明します。

まず、下記のプログラムを実行して、`f` を拡張前の関数に戻します。

```python
def f():
    print("関数 f を実行しました") 
```

次に、下記のような `f_show_time` というラッパー関数を定義します。なお、ラッパー関数の名前は何でも構いません。`wrapper` のような名前が使われることもあります。

```python
1  def f_show_time():
2      starttime = datetime.now()
3      f()
4      endtime = datetime.now()
5      print(endtime - starttime)
```

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

```python
def f_show_time():
    starttime = datetime.now()
    f()
    endtime = datetime.now()
    print(endtime - starttime)
```
</details>

先程の、関数の定義を修正する方法との違いは、以下の通りです。

* 関数 `f` の __定義を変更しない__
* __関数 `f` の処理__ は、`f_show_time` の中の 3 行目で __`f` を呼び出すことで行う__
* `f` の呼び出しの __前後__ に、`f` の機能を __拡張する処理を記述__ する

下記のプログラムのように、__`f_show_time()`__ を呼び出すと、`f` の処理時間が表示されるので、ラッパー関数によって __`f` の機能が拡張された__ ことが確認できます。

```python
f_show_time()
```

実行結果

```
関数 f を実行しました
0:00:00
```

同様の方法で、下記のプログラムで関数 `g` をラッパー関数 __`g_show_time`__ を定義する事で拡張することができます。

```python
def g():
    print("関数 g を実行しました") 
    
def g_show_time():
    starttime = datetime.now()
    g()
    endtime = datetime.now()
    print(endtime - starttime)
```

下記のプログラムを実行すると、実行結果から __`g` が拡張された__ ことが確認できます。

```python
g_show_time()
```

実行結果

```
関数 g を実行しました
0:00:00
```

### ラッパー関数の性質

ラッパー関数は、ラップする関数を __そのまま呼び出して利用する__ ので、__ラップする関数__ の中で行われる __処理を直接変更することはできません__。ラッパー関数が行うのは、ラップする関数を __呼び出す前に行う「前処理」__ とラップする関数を __呼び出した後で行う「後処理」__ を記述することで、__間接的に機能を追加する__ というものです。

なお、「別の関数をその中で呼び出す関数」をラッパー関数と定義してしまうと、ほとんどの関数がラッパー関数に分類されてしまいます。そのため一般的にラッパー関数は、__既に定義されている関数の機能を変化させる目的__ で定義された関数のことを指します[^1]。

例えば、下記の仮引数 `x` と `y` の合計を表示する `print_sum` という関数は、その中で `print` を呼び出していますが、__`print_sum`__ は __`print` の機能を変化させるための関数ではない__ ので、一般的には __ラッパー関数とは呼ばない__ と思います。

```python
def print_sum(x, y):
    print(x + y)
```

一方、ai.py で定義した下記の __[`dprint`](https://qiita.com/ysgeso/items/474db9ea1805993eaa34#%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E8%A1%A8%E7%A4%BA%E3%82%92%E8%A1%8C%E3%81%86%E9%96%A2%E6%95%B0%E3%81%AE%E5%AE%9A%E7%BE%A9)__ は仮引数 `debug` の値が `True` の場合のみ `print` で表示を行うという、__`print` という関数の機能を変化する目的で定義__ されているので、__ラッパー関数です__。

```python
def dprint(debug, *args, **kwargs):
    if debug:
        print(*args, **kwargs)
```

:::note
上記で、関数の機能を拡張するのではなく、__機能を変更する__ と述べたのは、上記の `dprint` は、特定の条件を満たした場合のみ `print` を実行するので、`print` の __機能を制限しているとみなす__ ことができるからです。

ただし、`dprint` は元の `print` には無かった、条件によって表示を行わないという機能が追加されていると考えることもできるので、機能を拡張するという表現が必ずしも間違っているとは言えないと思います。
:::

# ラッパー関数の主な用途

__ラッパー関数__ は `f_show_time` のような機能の拡張以外にも、__様々な目的で利用される__ ので、それらについて紹介します。なお、下記以外でもラッパー関数は利用されます。

なお、__ラッパー関数__ は、実は __これまでの記事で既にいくつか定義している__ ので、なるべくそれらを具体例に挙げながら説明します。

## 関数呼び出しの記述の簡略化

__仮引数を多く持つ関数__ を呼び出す際に、対応する実引数を __記述するのは大変__ なだけでなく、__プログラムがわかりづらくなる__ という問題があります。

そこで、関数の __特定の仮引数に常に同じ値を代入__ して関数を利用する場合は __ラッパー関数を定義__ する事で、__関数呼び出しの記述を簡略化する__ ということが良く行われます。

具体例としては、以前の記事で説明した、__再帰呼び出しによる繰り返し処理__ が挙げられます。下記は、[以前の記事](https://qiita.com/ysgeso/items/8432194829a4c6cc95f0#0-%E3%81%8B%E3%82%89-n-%E3%81%BE%E3%81%A7%E3%81%AE%E6%95%B4%E6%95%B0%E3%81%AE%E5%90%88%E8%A8%88%E3%81%AE%E8%A8%88%E7%AE%97%E3%82%92%E8%A1%8C%E3%81%86%E9%96%A2%E6%95%B0%E3%81%AE%E5%AE%9A%E7%BE%A9)で定義した、ボトムアップな再帰呼び出しによって 0 から n までの整数の合計を計算する関数です。

```python
def sum_by_br(n, i, total):
    if i <= n:
        total += i
        i += 1
        return sum_by_br(n, i, total)
    else:
        return total
```

この関数を利用する際には、下記のプログラムのように、__仮引数 `i` と `total` に `0` を代入して呼び出す必要__ があります。

```python
print(sum_by_br(n=10, i=0, total=0))
```

実行結果

```
55
```

__最初に呼び出す際__ の __仮引数 `i` と `total` の値が決まっている__ ので、下記のような `sum_by_br` をラップする __ラッパ関数 `sum_by_br_from_0` を定義__ することで、その下のプログラムのように、ラッパ関数に __`n` の値のみを実引数に記述__ して 0 から n までの整数の合計を計算できるようになります。

```python
def sum_by_br_from_0(n):
    return sum_by_br(n, i=0, total=0)
```

```python
print(sum_by_br_from_0(10))
```

実行結果

```
55
```

このようなラッパー関数は、ラップする関数の一部の __仮引数に代入する値を特定の値に限定する__ ので、機能を拡張するのではなく、逆に __機能を制限する__ 役割を持ちます、

:::note
上記の例の場合は、ラッパ関数ではなく下記のプログラムのように __デフォルト引数を利用する__ 方法があり、こちらの方がより簡潔にプログラムを実装できます。

ただし、__デフォルト値は一つしか設定できない__ ので、__デフォルト値以外の値__ を仮引数に常に代入して呼び出すような場合は、__デフォルト引数は利用できません__。

```python
def sum_by_br(n, i=0, total=0):
    if i <= n:
        total += i
        i += 1
        return sum_by_br(n, i, total)
    else:
        return total
```
:::

:::note
__再帰呼び出し__ でラッパ関数を定義する場合は、下記のプログラムのように __ラッパ関数の名前__ を __`sum_n`__ のように __行う処理を表す名前__ にし、__再帰呼び出しの関数__ をラッパ関数の __名前の末尾に `_r`__[^2]をつけた名前にする場合が多いようです。

```python
def sum_n_r(n, i, total):
    if i <= n:
        total += i
        i += 1
        return sum_n_r(n, i, total)
    else:
        return total

def sum_n(n):
    return sum_n_r(n, i=0, total=0)

print(sum_n(10))
```

実行結果

```
55
```

また、__再帰呼び出しを行う関数__ を、__他の場所から呼び出すことがない__ ことがあらかじめわかっている場合は、下記のプログラムのように、再帰呼び出しの関数を __ラッパ関数の中にローカル関数として定義__ することが良くあります。

```python
def sum_n(n):
    def sum_n_r(n, i, total):
        if i <= n:
            total += i
            i += 1
            return sum_n_r(n, i, total)
        else:
            return total

    return sum_n_r(n, i=0, total=0)

print(sum_n(10))
```

実行結果

```
55
```
:::


下記は、Mbtree クラスの中で定義されている、再帰呼び出しによってゲーム木を作成する __[`create_tree_by_df`](https://qiita.com/ysgeso/items/38f2c8167a14eb99d86c#%E5%86%8D%E5%B8%B0%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E3%81%AB%E3%82%88%E3%82%8B%E5%AE%9F%E8%A3%85-1)__ メソッドの定義の先頭の行の部分です。

```python
def create_tree_by_df(self, node):
略
```

この関数を利用してゲーム木を作成する際は、下記のプログラムのように、__ゲーム木のルートノードを作成__ し、__仮引数 `node`__ に対応する実引数に、作成した __ルートノードを記述して呼び出す__ 必要があります。なお、下記のプログラムは `__init__` メソッドの中から関連する処理のみを抜き出したものです。

```python
self.root = Node(Marubatsu())
self.tree = self.create_tree_by_df(self.root)
```

実際の Mbtree クラスでは定義していませんが、この __一連の処理行う__ 下記の __ラッパーメソッドを定義__ することで、__`self.create_tree_by_df_from_root()`__ のように、__ルートノードの作成の処理__ や、__実引数を記述することなくゲーム木を作成__ することができるようになります。

```python
def create_tree_by_df_from_root(self):
    self.root = Node(Marubatsu())
    self.tree = self.create_tree_by_df(self.root)
```

## データ構造の変更

プログラムでは、__同じ意味を持つデータ__ を __様々なデータ構造で表現__ することができます。例えば、__座標を表すデータ構造__ は「`(x, y)` という tuple」や「`"x,y"` という文字列」[^3]などの、__様々な方法で表現できます__。

そのため、__他人が作ったモジュール__ で利用されている __データ構造__ が、__自分のプログラム__ で利用している __データ構造と異なる__ ことが良くあります。例えば、__自分のプログラム__ では座標を表すデータを __tuple で表現__ しているが、__インポートしたモジュール__ では __文字列で表現__ されているような場合などが考えられるでしょう。

このような場合は、__ラッパ関数を定義してデータ構造を変更する__ ことができます。

下記の __`exchange_xy`__ は、仮引数 __`coord_txt`__ に __文字列で表現された座標__ を代入し、__x 座標と y 座標を入れ替えた文字列の座標を返す__ 関数です。なお、2 行目の `split` は、特定の文字列で区切られた文字列を list に変換する処理を行うメソッドです。`split` メソッドについて忘れた方は、[以前の記事](https://qiita.com/ysgeso/items/6ca248133ff4d59a81d4#split-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AB%E3%82%88%E3%82%8B-list-%E3%81%B8%E3%81%AE%E5%A4%89%E6%8F%9B) を復習して下さい。

```python
def exchange_xy(coord_txt):
    x, y = coord_txt.split(",")
    return f"{y},{x}"
```

下記のプログラムを実行すると、`"1,2"` という文字列の座標が `"2,1"` に変換されます。

```python
print(exchange_xy("1,2"))
```

実行結果

```
2,1
```

### 仮引数のデータ構造を変換するラッパ関数

上記の __`exchange_xy`__ が __別のモジュールで定義__ されており、インポートして自分のプログラムで利用しようと思った場合に、`exchange_xy` の仮引数 `coord_txt` に tuple で表現された座標のデータを代入することはできません（エラーが発生します）。そこで、下記のような __`exchange_xy_by_tuple`__ という __ラッパー関数を定義__ し、その中の 2、3 行目で仮引数 __`coord_tuple`__ に代入した __tuple を文字列の座標データに変換する処理を記述__ することで、仮引数に __文字列の座標データを代入して `exchange_xy` を呼び出す__ ことができるようになります。

```python
def exchange_xy_by_tuple(coord_tuple):
    x, y = coord_tuple
    coord_txt = f"{x},{y}"
    return exchange_xy(coord_txt)
```

下記のプログラムを実行すると、`(1, 2)` という tuple の座標が `"2,1"` という文字列の座標に変換されます。

```python
print(exchange_xy_by_tuple((1, 2)))
```

実行結果

```
2,1
```

### 返り値のデータ構造を変換するラッパ関数

上記の __`exchange_xy_by_tuple`__ の __返り値は文字列型のデータ__ なので、自分のプログラムに合わせて __tuple の座標データを返す__ ようにする必要があります。

下記は、そのように `exchange_xy_by_tuple` を修正したプログラムです。

* __5 行目__：返り値に対して `split` メソッドを利用することで、x 座標と y 座標の値を取り出して `x` と `y` に代入する
* __6 行目__：座標を表す tuple を計算して `retval_tuple` に代入する。なお、__`split`__ メソッドで作成した list の __要素は文字列型のデータ__ なので、組み込み関数 `float` を使って float 型のデータに __型変換を行う必要がある__ 点に注意すること
* __7 行目__：tuple に変換した座標データを返り値として返す

```python
1  def exchange_xy_by_tuple(coord_tuple):
2      x, y = coord_tuple
3      coord_txt = f"{x},{y}"
4      retval_txt = exchange_xy(coord_txt)
5      x, y = retval_txt.split(",")
6      retval_tuple = (float(x), float(y))
7      return retval_tuple
```

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

```python
def exchange_xy_by_tuple(coord_tuple):
    x, y = coord_tuple
    coord_txt = f"{x},{y}"
    retval_txt = exchange_xy(coord_txt)
    x, y = retval_txt.split(",")
    retval_tuple = (float(x), float(y))
    return retval_tuple
```
</details>

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

```diff_python
def exchange_xy_by_tuple(coord_tuple):
    x, y = coord_tuple
    coord_txt = f"{x},{y}"
-   return exchange_xy(coord_txt)
+   retval_txt = exchange_xy(coord_txt)
+   x, y = retval_txt.split(",")
+   retval_tuple = (float(x), float(y))
+   return retval_tuple
```
</details>

上記の修正後に下記のプログラムを実行すると、`(1, 2)` という tuple の座標が __`(2.0, 1.0)` という tuple の座標に変換__ されます。なお、`2.0` のように表示されるのは、__`float` を使って浮動小数点型のデータに変換したから__ ですが、数値の意味は __整数型の `2` と全く同じ__ なので __特に問題はありません__。

```python
print(exchange_xy_by_tuple((1, 2)))
```

実行結果

```
(2.0, 1.0)
```

### 単位を変換する例

上記ではデータ型の変換を行いましたが、ラッパー関数で __データの単位を変換する__ という例を紹介します。例えば、関数 __`f`__ が返り値として __秒の単位で時間を返す__ 場合は、下記のような __`f_minitue`__ というラッパ関数を定義することで、`f` の返り値を __分の単位に変換して返す__ ことができるようになります。

```python
def f_minitue()
    return f() / 60
```

## 機能の変更

先程説明したように、ラッパー関数を定義する事で、ラップした関数に __機能の追加や変更__ を行うことができます。先ほどは処理時間を表示する機能の追加例を紹介しましたが、ここでは __機能の変更例__ として、__反復可能オブジェクトの要素の合計を計算__ する組み込み関数 __[`sum`](https://qiita.com/ysgeso/items/a97a3cdf54087e662230#list-%E3%81%AE%E4%BD%9C%E6%88%90%E3%81%A8-sum-%E3%81%AB%E3%82%88%E3%82%8B%E8%A8%88%E7%AE%97)__ に、__角度の合計を計算する__ ように __機能を変更__ する __ラッパ関数を定義__ します。

__単位が度（degree）の角度__ は、__0 以上 360 未満の値__ を取りますが、__角度の合計の計算を `sum` 関数で行う__ と、下記のプログラムのように、__上記の範囲を超えるような値が計算__ されてしまします。

```python
print(sum([200, 300]))
```

実行結果
```
500
```

そこで、__`sum`__ による __合計の結果__ を __0 以上 360 未満の値に変換__ する __`sum_degree`__ という __ラッパ関数を定義__ することで、__角度の合計を計算できる__ ようになります。

* __1 行目__：`sum_degree` の仮引数の値を、`sum` を呼び出す際に実引数として記述できるように、仮引数 `*args` と `**kwargs` を記述する。この部分の意味を忘れた方は[以前の記事](https://qiita.com/ysgeso/items/474db9ea1805993eaa34#%E5%8F%AF%E5%A4%89%E9%95%B7%E5%BC%95%E6%95%B0%E3%81%A8%E5%AE%9F%E5%BC%95%E6%95%B0%E3%81%AE%E5%B1%95%E9%96%8B)を復習すること
* __2 行目__：`sum_degree` の仮引数の値を実引数に記述して `sum` を呼び出し、返り値を `degree` に代入する
* __3 行目__：`degree` の値を 0 以上 360 未満の値に変換するために、% 演算子で 360 で割った余りを計算する
* __4 行目__：`degree` の値を返り値として返す

```python
1  def sum_degree(*args, **kwargs):
2      degree = sum(*args, **kwargs)
3      degree %= 360
4      return degree
```

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

```python
def sum_degree(*args, **kwargs):
    degree = sum(*args, **kwargs)
    degree %= 360
    return degree
```
</details>

:::note
下記のプログラムのように、2 ~ 4 行目を 1 行でまとめることもできます。

```python
def sum_degree(*args, **kwargs):
    return sum(*args, **kwargs) % 360
```
:::

下記のプログラムを実行すると、200 と 360 の合計と、200 と -360 の合計が 200 になることから `sum_degree` で計算した値が 0 以上 360 未満になることが確認できます。

```python
print(sum_degree([200, 360]))
print(sum_degree([200, -360]))
```

実行結果

```
200
200
```

:::note
JavaScript など、プログラム言語によっては、負の値の余りを計算すると、答えが 0 以下の値になる場合がありますが、Python では必ず 0 以上の値になります。
:::

上記の例では、ラップする関数の __返り値を別の値にして返す__ という形で機能の変更を行いましたが、ラッパー関数の __仮引数の値を別の値にして__ ラップする関数の __実引数に記述して呼び出す__ という方法で機能の変更を行うこともできます。

## 関数の名前をわかりやすい名前に変える

__わかりづらいの関数の名前__ を、ラッパー関数で別の __わかりやすい名前に変える__ という使い方もできますが、Python ではインポートする関数の名前を変える場合は、__`import 名前 as 別の名前` を利用したほうが良いでしょう__。例えば `import matplotlib.pyplot as plt` は matplotlib モジュールの `pyplot` を `plt` という別の名前でインポートしています。

# ラップする関数を仮引数に代入するラッパー関数

先程紹介した関数 `f` に処理時間の機能を追加したラッパー関数 `f_show_time` は、__`f` という関数しか機能を拡張することができません__。そのため、__複数の関数__ に対して処理時間の機能の追加を行いたい場合は、関数の定義を修正する方法と同様に、それぞれの関数に対して __別々のラッパー関数を定義__ する必要があるため大変です

この問題に対して、__任意の関数__ に対して処理時間の機能を追加することができる __ラッパー関数を定義__ するという方法があります。そのようなラッパー関数を定義できれば、__関数ごとにラッパー関数を定義する必要がなくなる__ ので便利です。

そのようなラッパー関数は、下記のプログラムのように、__処理時間を計測する関数を代入__ する __仮引数を追加__ することで定義する事ができます。なお、ラッパー関数の名前は任意の関数の処理時間を計測するので `show_time` としました。

* __1 行目__：処理時間を計測する関数を代入する仮引数 `func` を追加する
* __3 行目__：`func` を呼び出すように修正する

```python
1  def show_time(func):
2      starttime = datetime.now()
3      func()
4      endtime = datetime.now()
5      print(endtime - starttime)
```

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

```python
def show_time(func):
    starttime = datetime.now()
    func()
    endtime = datetime.now()
    print(endtime - starttime)
```
</details>

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

```diff_python
-def f_show_time(func):
+def show_time(func):
    starttime = datetime.now()
-   f()
+   func()
    endtime = datetime.now()
    print(endtime - starttime)
```
</details>

:::note
これまで言及していませんでしたが、__関数を引数として代入する関数__ のことを __高階関数__（high order function）と呼びます。今後の記事で紹介する予定ですが、__関数を返り値として返す関数__ も __高階関数__ です。
:::

上記の修正後に下記のプログラムのように、__ラッパ関数の実引数に `f` を記述して呼び出す__ ことで、実行結果のように関数 `f` の処理時間が表示されることが確認できます。

```python
show_time(f)
```

実行結果

```
関数 f を実行しました
0:00:00
```

同様に、下記のプログラムで関数 `g` の処理時間を計測することができます。

```python
show_time(g)
```

実行結果

```
関数 g を実行しました
0:00:00
```

このように、__ラッパ関数の仮引数__ に、機能を拡張したい __関数を代入する仮引数を追加__ し、ラッパ関数の中で __その関数を呼び出す処理を記述する__ ことで、__1 つのラッパ関数で任意の関数の機能を変更する__ ことができるようになります。

## 仮引数と返り値を持つ関数のラッパー関数

上記のラッパー関数は、__仮引数を持つ関数__ や、__返り値を返す関数__ に __対応していません__。任意の仮引数を持つ関数や、返り値を持つ関数に対応するためには、下記のプログラムのようにラッパー関数を定義します。

* __1 行目__：仮引数 `*args` と `**kwargs` を追加する
* __3 行目__：実引数に `*args` と `**kwargs` を記述して `func` を呼び出す。また、その __返り値を `retval` に代入__ する。これは、4、5 行目で行う `func` を呼び出した後の __後処理を行った後で返り値を返す__ ことができるようにするためである
* __6 行目__：関数の最後の処理で、`retval` を返り値として返す

```python
1  def show_time(func, *args, **kwargs):
2      starttime = datetime.now()
3      retval = func(*args, **kwargs)
4      endtime = datetime.now()
5      print(endtime - starttime)
6      return retval
```

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

```python
def show_time(func, *args, **kwargs):
    starttime = datetime.now()
    retval = func(*args, **kwargs)
    endtime = datetime.now()
    print(endtime - starttime)
    return retval
```
</details>

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

```diff_python
-def show_time(func):
+def show_time(func, *args, **kwargs):
    starttime = datetime.now()
-   func()
+   retval = func(*args, **kwargs)
    endtime = datetime.now()
    print(endtime - starttime)
+   return retval
```
</details>

上記の修正後に下記のプログラムを実行することで、仮引数と返り値を持つ関数 `sum` に対して`sum([100, 200])` を呼び出した際の __処理時間が計測__ されることが実行結果から確認できます。また、`show_time` は `func(*args, **kwargs)` の返り値を返すようになったので、実行結果の最後に `sum([100, 200])` の __返り値の値が表示__ されます。

```python
print(show_time(sum, [100, 200]))
```

実行結果

```
0:00:00
300
```

また、__`show_time`__ は __任意の関数__ の __処理時間を計測できる__ ので、`sum` の __ラッパー関数である `sum_degree` に対しても__、下記のプログラムのように __処理時間を計測できます__。

```python
print(show_time(sum_degree, [100, 360]))
```

実行結果

```
0:00:00
100
```

:::note
ラッパー関数は、その中で別の関数を呼び出す関数のことなので、ラッパー関数という __特別な種類の関数が存在するわけではありません__。そのため、上記のように、__ラッパー関数に対して別のラッパー関数を定義__ することができます。
:::

# 本記事で定義済のラッパー関数

__関数を仮引数に代入する関数__ は、本記事では __既にいくつか定義済__ です。例えば __Marubatsu__ クラスの __`play`__ メソッドは __AI の関数を仮引数に代入__ します。ただし、__`play`__ メソッドが行う処理は __〇×ゲームを遊ぶ処理__ で、__AI の関数の機能を利用する処理がメインではありません__。例えば __人間どうしが対戦を行う場合__ はそもそも __AI の関数を利用しない__ ので、`play` メソッドは AI の関数を拡張した __ラッパー関数とは言えない__ でしょう。

既に定義した関数の中で __ラッパー関数と言える__ のは __仮引数 `eval_func`__ に __AI の評価関数を代入__ する __`ai_by_score`__ です。この関数は、__AI の評価関数を利用__ して仮引数 `mb` に代入した局面に対する __AI の着手を計算する処理を行う__ ため、__評価関数の機能を拡張している__ と考えることができるので、ラッパー関数と言えるでしょう。

また、下記のプログラムのように、前回の記事で修正した __`ai3s`__ はその中で __`ai_by_score` を呼び出している__ ので、__`ai3` は `ai_by_score` のラッパー関数__ と言えるでしょう。

```python
def ai3s(mb, debug=False, analyze=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, analyze=analyze)
```

また、下記のプログラムの __`ai3s(mb)` と同じ処理__ は、その下のプログラムで __`ai_by_score`__ を呼び出して __行うことができる__ ことからも、__`ai3s` は `ai_by_score` のラッパー関数__ であると言えるでしょう。実際に実行結果のようにどちらも同じ返り値を返します。

```python
from ai import ai3
from marubatsu import Marubatsu

mb = Marubatsu()
print(ai3(mb))
```

実行結果

```
(1, 1)
```

```python
from ai import ai_by_score

def eval_func(mb):
    if mb.last_move == (1, 1):
        return 1
    else:
        return 0

mb = Marubatsu()
ai_by_score(mb, eval_func, debug=False, analyze=False)
```

実行結果

```
(1, 1)
```

# 今回の記事のまとめ

今回の記事では、次回の記事で説明する __デコレーターを理解するための基礎知識__ となる __ラッパー関数__ について説明しました。次回の記事では、デコレータについて説明し、デコレータを利用した AI の関数の定義の方法について説明します。

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

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

# 次回の記事

https://qiita.com/ysgeso/items/18ad5c20101c96aaecae

[^1]:ラッパー関数の厳密な定義は決まっていないと思います。そのため、文献や人によってはラッパー関数を若干異なる意味で使われていると思います
[^2]:再帰呼び出し（recursive call）の頭文字を表す `r` です
[^3]:`(x, y)` や `"x,y"` の x と y には、それぞれ x 座標と y 座標を表す数字が入ります