Pythonで〇×ゲームのAIを一から作成する　その130　クラスによるデコレーターの定義とデコレーター式による AI の定義

# 目次と前回の記事

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

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

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

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

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

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

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

# クラスによるデコレーターの定義

[前回の記事](https://qiita.com/ysgeso/items/bd63433b7491d8a07ff5#%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF%E3%83%BC%E5%BC%8F%E3%81%AE%E7%A8%AE%E9%A1%9E)で、__デコレーター式の種類__ について説明を行いましたので、その続きを説明します。[前回の記事](https://qiita.com/ysgeso/items/bd63433b7491d8a07ff5#%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF%E3%83%BC%E5%BC%8F%E3%81%AB%E3%82%88%E3%82%8B%E3%83%A9%E3%83%83%E3%83%91%E3%83%BC%E9%96%A2%E6%95%B0%E3%81%AE%E5%AE%9A%E7%BE%A9)では、__デコレーター式__ によるラッパー関数の定義では `@` の直後に下記のような性質を持つ[デコレーター](https://qiita.com/ysgeso/items/bd63433b7491d8a07ff5#%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B)の __関数名を記述__ すると説明しました。

* ラップする関数を代入する __仮引数を 1 つだけ持つ__
* 返り値として __関数を返す__

実際には、__上記のような性質を持つオブジェクト__ であれば、デコレーター式の `@` の直後に記述するのは、__関数でなくてもかまいません__。

具体的には、以下のような __クラス__ をデコレーター式に記述することができます。

* インスタンスを作成する際に呼び出される __`__init__` メソッド__ に、ラップする関数を代入する __仮引数を 1 つだけ持つ__
* クラスから作成された __インスタンスに対して__ 、直接 __関数呼び出し__ のような __記述を行うことができる__

## 呼び出し可能オブジェクトと `__call__` メソッド

関数やメソッドのように、直後に `(実引数, ...)` を記述して __呼び出すことができるオブジェクト__ の事を Python では __呼び出し可能（callable）オブジェクト__ と呼びます。 

Python では、関数やメソッド以外に、クラスの __インスタンスを呼び出し可能オブジェクトにする__ 方法が用意されています。具体的には、クラスの定義に __`__call__` という特殊メソッドを定義__ すると、そのクラスのインスタンスが呼び出し可能オブジェクトになります。また、`__call__` メソッドが定義されたクラスの __インスタンスを呼び出す__ と、__`__call__` メソッド__ が呼び出されます。

具体例を挙げて説明します。下記のプログラムは、仮引数 `x`、`y` の合計を返り値として計算する処理を行う __`__call__` メソッドが定義__ された __Add クラスの定義__ です。

```python
class Add:
    def __call__(self, x, y):
        return x + y
```

__Add から作成__ されたインスタンスは __呼び出し可能オブジェクトになる__ ので、下記のプログラムの 2 行目の __`add(1, 2)` のように直接呼び出す__ ことができます。`add(1, 2)` が行う処理は、__`add.__call__(1, 2)` と同じ__ なので、実行結果のように 3 が表示されます。

```python
add = Add()
print(add(1, 2))
```

実行結果
```
3
```

:::note
上記の Add クラスは、呼び出し可能オブジェクトを __クラスのインスタンスで作成する方法を説明__ するための例なので、__実用的なクラスではありません__。

2 つの仮引数の合計を返す処理を行うという、呼び出し可能オブジェクトを作成するだけであれば、下記のような __`add` 関数を定義したほうが良い__ でしょう。

```python
def add(x, y):
    return x + y
```
:::

呼び出し可能オブジェクトの詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/glossary.html#term-callable

`__call__` メソッドの詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/reference/datamodel.html#emulating-callable-objects

## 呼び出し可能オブジェクトの呼び出しの処理の仕組み

呼び出し可能オブジェクトを __呼び出した際__ に行われる __処理の仕組み__ を説明します。

[公式のドキュメント](https://docs.python.org/ja/3/reference/datamodel.html#object.__call__)には、`x` という呼び出し可能オブジェクトに対して、 __`x(arg1, arg2, ...)`__ のような呼び出しを行った場合は、おおまかに（roughly）__`type(x).__call__(x, arg1, arg2, ...)`__ と同じ処理を行うと説明されています。この説明について解説します。

:::note
公式のドキュメントには「おおまかに同じ処理」とあるので、厳密には同じ処理ではないかもしれませんが、呼び出し可能オブジェクトの呼び出しの処理の仕組みを理解する上では、同じ処理だと考えても問題はないと思います。
:::

### 組み込み関数 `type`

__`type`__ は、__オブジェクトのデータ型を返す__ 処理を行う組み込み関数で、一般的にはその __オブジェクトを作成したクラス__ が得られます。例えば、先程 __Add クラスから作成__ したインスタンスを代入した `add` に対して `type(add)` を実行すると、実行結果のように __Add クラスが得られたことが確認__ できます。なお、実行結果に表示される __`__main__.Add`__ は、Add クラスが __メインモジュールで定義されている__ ことを表します。__JupyterLab に記述して実行__ したプログラムは、[以前の記事](https://qiita.com/ysgeso/items/cd3e1c0731b859bf5e2c#%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93)で説明したように、__メインモジュールのプログラム__ です。

```python
print(type(add))
```

実行結果

```
<class '__main__.Add'>
```

__def 文__ によって定義された __関数オブジェクト__ は、__function__ という組み込みクラス __から作成されたインスタンス__ です。そのため、下記のプログラムを実行して作成された __`add_func`__[^1] 関数に対して __`type(add_func)`__ を実行すると、実行結果のように __function クラス__ が得られたことが確認できます。なお、組み込み関数の場合は、実行結果のようにクラス名の前には何も表示されません。

```python
def add_func(x, y):
    return x + y

print(type(add_func))
```

実行結果

```
<class 'function'>
```

組み込み関数 `type` の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/functions.html#type

### `type(x).__call__(x, arg1, arg2, ...)` が行う処理の意味

上記から、`x` がクラス ClassX のインスタンスの場合は、__`type(x)`__ によって __クラス ClassX が返される__ ことになります。従って、__`type(x).__call__(x, arg1, arg2, ...)`__ は、__`ClassX.__call__(x, arg1, arg2, ...)`__ という処理を行います。

[以前の記事](https://qiita.com/ysgeso/items/44884dcf168aab949508#%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)で説明したように、クラス ClassX のインスタンスである `x` から クラス ClassX の `a` というメソッドを __`x.a(arg1, arg2, ...)`__ のように呼び出すと、__`ClassX.a(x, arg1, arg2, ...)`__ という処理が行われます[^2]。従って、__`ClassX.__call__(x, arg1, arg2, ...)`__ という処理は、__`x.__call__(arg1, arg2, ...)`__ と __同じ処理__ を行います。

公式のドキュメントに記載されている、「__`x(arg1, arg2, ...)`__ のような呼び出しを行った場合は、おおまかに（roughly）__`type(x).__call__(x, arg1, arg2, ...)`__ と同じ処理を行う」という記述と、上記のことから、以下の事がわかります。

:::note warn
__`x(arg1, arg2, ...)`__ のような呼び出しを行った場合は、__`x.__call__(arg1, arg2, ...)`__ と同じ処理が行われる。従って、__呼び出しの処理__ は、`__call__` メソッドを呼び出す処理の __`.__call__` の部分を省略した記法__ と考えることができる。
:::

### 呼び出し可能オブジェクトと `__call__` メソッド

上記のことから、__呼び出し可能オブジェクト__ には、__必ず `__call__` メソッドが定義__ されていなければならないことがわかります。そのため、関数やメソッドを表す __関数オブジェクト__ には __特殊メソッド `__call__` が定義__ されています。

実際に、先程定義した __`add`__ 関数には __`__call__` メソッドが定義__ されているので、下記のプログラムのように __`__call__` メソッドを呼び出す__ ことでも __`add` 関数で定義した処理を行う__ ことができ、実行結果のように `add(1, 2)` と同じ返り値が返ることが確認できます。

```python
print(add.__call__(1, 2))
```

実行結果
```
3
```

クラスの __メソッドも同様__ で、下記のプログラムで乗算（multiply）を行う __`mul` というメソッド__ を持つ __Mul というクラスを定義__ して実行すると、クラス Mul の __属性 `mul` に代入__ される __関数オブジェクト__ には、特殊メソッド __`__call__` が定義__ されます。

```python
class Mul:
    def mul(self, x, y):
        return x * y
```

そのため、`mul` メソッドを表す関数オブジェクトには `__call__` メソッドが定義されているので、下記のプログラムの 2 行目と 3 行目は同じ処理を行います。

```python
mul = Mul()
print(mul.mul(1, 2))
print(mul.mul.__call__(1, 2))
```

実行結果
```
2
2
```

__def 文__ による __関数やメソッドの定義__ を実行した際に作成される関数オブジェクトに __`__call__` メソッドが定義される__ のは、関数オブジェクトが __function クラスから作成されたインスタンス__ であり、function クラスに __`__call__` メソッドが定義されている__ からです。

__クラスを自分で定義__ する場合は、`__call__` メソッドが自動的に定義されることはないので、__インスタンスを呼び出し可能オブジェクトとしたい__ 場合は、__自分で `__call__` メソッドを定義する必要__ があります。

## クラスによるデコレーターの定義の例

次に、__クラスを利用__ して、__`create_show_time` と同じ処理__ を行う __デコレーターを定義__ する方法を説明します。比較するために `create_show_time` の関数の定義を再掲します。

```python
from datetime import datetime

def create_show_time(func):
    def show_time(*args, **kwargs):
        starttime = datetime.now()
        retval = func(*args, **kwargs)
        endtime = datetime.now()
        print(endtime - starttime)
        return retval
    
    return show_time
```

先程説明したように、デコレータ式に記述クラスは、下記の性質を持つ必要があります。

* インスタンスを作成する際に呼び出される __`__init__` メソッド__ に、ラップする関数を代入する __仮引数を 1 つだけ持つ__
* クラスから作成された __インスタンスに対して__ 、直接 __関数呼び出し__ のような __記述を行うことができる__

__`create_show_time` と同じ処理__ を行う __デコレーターのクラス__ は、下記のプログラムで定義できます。なお、クラスの名前は __`Showtime`__ としました。

* __1 行目__：`Showtime` クラスを定義する。__クラスの定義には仮引数を記述しない__ ので、 `create_show_time` の定義に記述されていた仮引数 `func` を記述してはいけない[^3]
* __2、3 行目__：インスタンスを作成する際に呼び出される `__init__` メソッドを定義する。__その仮引数に `func` を記述__ し、3 行目で同名の属性に代入する
* __5 行目__：特殊メソッド `__call__` を定義する。メソッドなので、最初の仮引数に `self` を記述する点を除けば __ラッパー関数 `show_time` の定義と同じ__
* __6 ~ 10 行目__：行う処理は `show_time` と同じだが、7 行目の __[クロージャー変数](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) `func`__ が、__インスタンスの属性の `self.func`__ になる点が異なる
* __`create_show_time`__ では、定義した __ラッパー関数を `return show_time` で返す__ 処理を行っていたが、クラスから作成された __インスタンスがラッパー関数の役割を果たす__ ため、__その処理は必要がなくなったので削除__ する

```python
 1  class Showtime:
 2      def __init__(self, func):
 3          self.func = func
 4       
 5      def __call__(self, *args, **kwargs):
 6          starttime = datetime.now()
 7          retval = self.func(*args, **kwargs)
 8          endtime = datetime.now()
 9          print(endtime - starttime)
10          return retval
```

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

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

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

```diff_python
-def create_show_time(func):
+class Showtime:
+   def __init__(self, func):
+       self.func = func
        
-   def show_time(*args, **kwargs):
+   def __call__(self, *args, **kwargs):
        starttime = datetime.now()
-       retval = func(*args, **kwargs)
+       retval = self.func(*args, **kwargs)
        endtime = datetime.now()
        print(endtime - starttime)
        return retval

-   return show_time
```
</details>

上記で定義した __`Showtime` クラス__ は __デコレーターの性質を持つ__ ので、下記のプログラムのように __デコレーター式に記述__ して `add_1` の __ラッパー関数を定義__ する事ができます。

```python
@Showtime
def add_1(x):
    return x + 1
```

下記のプログラムを実行することで、実行結果から、`add_1` が処理時間を表示する機能を持つラッパー関数になっていることが確認できます。

```python
print(add_1(1))
```

実行結果

```
0:00:00
2
```

## クラスによるデコレーター式が行う処理

[前回の記事](https://qiita.com/ysgeso/items/bd63433b7491d8a07ff5#%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF%E3%83%BC%E5%BC%8F%E3%81%8C%E8%A1%8C%E3%81%86%E5%87%A6%E7%90%86)で説明したように、下記の 1 ~ 3 行目の __デコレーター式が行う処理__ は、__5 ~ 7 行目と同じ__ 処理を行います。そのことは、デコレーター式に記述した名前が関数であっても、クラスであっても変わることはありません。

```python
1  @Showtime
2  def add_1(x):
3      return x + 1
4
5  def add_1(x):
6      return x + 1
7  add_1 = Showtime(add_1)
```

7 行目の処理によって、__Showtime のインスタンスが作成__ され、__`add_1` に代入__ されますが、その際に Showtime の __`__init__` メソッドが実行__ され、インスタンスの __`func` 属性__ には、5、6 行目で定義された __ラップする関数の関数オブジェクトが代入__ されます。

__Showtime__ には __`__call__` メソッドが定義__ されているので、__`add_1`__ に代入された Showtime のインスタンスは __呼び出し可能オブジェクト__ です。そのため、下記のプログラムを実行して Showtime のインスタンスを呼び出すと __`__call__` メソッドが呼び出されます__。

```python
print(add_1(1))
```

__`add_1`__ の __`func` 属性__ には 5、6 行目で定義された __ラップする関数の関数オブジェクトが代入__ されているので、`__call__` メソッドの中で実行される __`retval = self.func(*args, **kwargs)`__ では、__ラップする関数が呼び出されます__。


## 2 つのデコレータの定義の方法の違い

上記のように、__デコレーター__ は __関数とクラス__ の __2 種類の方法で定義__ する事ができます。それぞれが行う処理の違いを表にまとめます。

|  | 関数 | クラス |
|:--|:--|:--|
| 行う処理 | __ラッパー関数__ を定義して返す | __ラッパー関数と同様の機能__ を持つ、呼び出し可能なクラスの __インスタンスを作成__ する | 
| ラップする関数の記録  | __[クロージャー変数](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)__<br>デコレーターのローカル名前空間に記録  | __インスタンスの属性__<br>[インスタンスの名前空間](https://qiita.com/ysgeso/items/44884dcf168aab949508#%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%81%AE%E4%BD%9C%E6%88%90%E3%81%A8%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93)に記録  |
| ラッパー関数の定義  | __ローカル関数__ で定義  |  __`__call__` メソッド__ で定義 |

表から両者が大きく異なる処理を行っているように見えるかもしれませんが、両者が行っている処理は __本質的には同じ__ です。

行う処理は、それぞれ「関数オブジェクト」と「クラスのインスタンス」を返すという点で異なります。ただだし、どちらの処理も __呼び出し可能オブジェクトを返す__ という点で同じ処理を行っています。異なるのは、呼び出した際に実行される `__call__` メソッドが下記のようにどのように作られるかです。

* __def 文__ による関数の定義の場合は `__call__` メソッドが __自動的に定義__ される
* __クラス__ のほうは、`__call__` メソッドを __自分で定義__ する

__ラップする関数の記録__ は、それぞれ __クロージャー変数__ と __インスタンスの属性__ で行われるという点で異なります。ただし、どちらのも作成した __呼び出し可能オブジェクトが参照__ する __名前空間に記録__ されているという点は同じです。

__ラッパー関数の定義__ は、どちらも __def 文によって定義される__ 点は同じです。

## クロージャーを利用したオブジェクト指向の記述

2 つの方法の違いは、作成するラッパー関数が「関数オブジェクト」であるか「クラスのインスタンス」であるかと、ラップする関数の記録を __[クロージャー](https://qiita.com/ysgeso/items/18ad5c20101c96aaecae#%E3%82%AF%E3%83%AD%E3%83%BC%E3%82%B8%E3%83%A3%E3%83%BC)__ の仕組みを利用した「クロージャー変数」に記録するか、「インスタンスの属性」に記録するかですが、どちらも行っている処理は上記のように本質的には変わりません。

そのため、__クロージャーを利用__ することで、クラスと全く同じではありませんが、__クラスと同じようなオブジェクト指向の処理を記述__ することができます。

例えば、下記のような 属性 `x`、`y` と、`x`、`y` の値を表示するメソッドを持つクラス XY とほぼ同様の機能を持つ関数を、クロージャーの仕組みを使って定義する事ができます。

```python
class XY:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def printx(self):
        print(self.x)

    def printy(self):
        print(self.y)
```

上記を実行後に、下記のプログラムで XY のインスタンスを作成し、`printx`、`printy` メソッドを呼び出すと、下記のような実行結果が表示されます。

```python
xy = XY(1, 2)
xy.printx()
xy.printy()
```

実行結果

```
1
2
```

__クラス XY__ と __ほぼ同じ処理を行う関数__ は、下記のプログラムのように定義できます。下記のプログラムは、__2 つのローカル関数を定義__ し、その中で __クロージャー変数 `x`、`y` を表示__ する処理を行います。また、返り値として __2 つのローカル関数を dict で返します__。

```python
def create_xy(x, y):
    def printx():
        print(x)

    def printy():
        print(y)

    return { "printx": printx, "printy": printy }
```

上記を実行後に、下記のプログラムで `create_xy(1, 2)` を呼び出すと、2 つのローカル関数が代入された dict が返り値として返ります。その返り値を使って、作成された 2 つの関数を呼び出すと、下記のように先程と同様の実行結果が表示されます。

```python
xy2 = create_xy(1, 2)
xy2["printx"]()
xy2["printy"]()
```

実行結果

```
1
2
```

## クロージャーとインスタンスの違いと使い分け

上記のように、__クロージャーを利用__ して、__クラスとほぼ同様の機能を実装__ することができますが、両者の間には __いくつかの違い__ や __利点と欠点__ があります。

### インスタンスの属性とクロージャー変数に対する参照と代入

大きな違いとして、__クラスの属性__ は、__`.属性名` を記述__ することで、インスタンスから __直接値を参照、代入する__ ことができますが、__クロージャー変数の値__ は、クロージャーの関数オブジェクトから __直接値を参照、代入することはできず__、クロージャーの __関数呼び出しの中の処理__ で値を参照、代入する必要があります。この性質は、__隠蔽__ と呼ばれ、クロージャー変数をクロージャーの外から勝手に参照、代入されることがないので、__データの保守性を高める__ ことができるという利点が得られます。

### ローカル変数との区別

インスタンスの属性は、`self.` を先頭に付けるのでローカル変数との区別がすぐ付けられますが、__クロージャー変数__ は、__ローカル変数と区別が付けづらい__ という欠点があります。

### 代入処理の違い

インスタンスの属性は、メソッドの処理の中で参照と代入のどちらも行うことができますが、__クロージャー変数__ は、クロージャーの関数呼び出しの処理の中で参照を行うことはできますが、__そのままでは代入処理を行うことはできません__。その理由は、[以前の記事](https://qiita.com/ysgeso/items/cd3e1c0731b859bf5e2c#%E4%BB%A3%E5%85%A5%E5%87%A6%E7%90%86%E3%81%AB%E3%82%88%E3%82%8B%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93%E3%81%B8%E3%81%AE%E7%99%BB%E9%8C%B2%E3%81%A8%E6%9B%B4%E6%96%B0)で説明したように、__名前解決のルール__ から、__クロージャー変数と同じ名前の変数に代入__ を行うと、__ローカル変数に対する代入処理になる__ からです。

__クロージャー変数に対する代入処理__ を行いたい場合は、__`nonlocal クロージャー変数名`__ をクロージャーの関数の中で記述する必要があります。

クロージャー変数のこの性質は、関数の処理の中で __グローバル変数に代入処理を行えない__ 点と似ています。また、`nonlocal` は [以前の記事](https://qiita.com/ysgeso/items/b0732427b90a1ec27243#global-%E3%82%92%E4%BD%BF%E3%81%86%E6%96%B9%E6%B3%95)で説明した、関数の中でグローバル変数に対する代入処理を行う際に、__`global グローバル変数名`__ を記述する必要があるのと同じです。

`nonlocal` の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/reference/simple_stmts.html#the-nonlocal-statement

### 呼び出すことができる処理の種類

__クロージャー__ は、__それ自体が関数オブジェクト__ なので、__呼び出すことができる処理__ は基本的には __1 種類__ しかありません。その代わり、今回の記事の最初で定義した [`Add` クラス](#呼び出し可能オブジェクトと-__call__-メソッド) と [`add_func`](#組み込み関数-type) のように、呼び出す処理が __1 種類しかない__ 場合は、__クロージャーの関数の定義__ のほうが、クラスより __簡潔に記述できる__ という利点があります。

それに対して、クラスの場合は、__メソッドを複数定義__ することで、同じインスタンスに対して __複数のメソッド__ を呼び出して __使い分ける__ ことができます。一応、__クロージャーの場合__ も上記の __`create_xy`__ のように、__同じクロージャー変数を共有する__、__複数のクロージャーを定義__ して使い分けることは可能ですが、それらを使い分けるための記述方法は、上記のように __クラスと比べてかなり複雑__ になります。

### 両者の使い分け

上記の事から、__呼び出す処理の種類が 1 種類__ しかない場合は __クロージャーの関数__ を、そうでない場合は、クラスを利用するという使い分けがあります。

__デコレータ__ の場合は、基本的にラップする関数の機能を拡張した __ラッパー関数を 1 種類だけ定義する__ ので、クラスによるデコレータを定義するメリットはほとんどないため、__クロージャーの関数でデコレーターを定義するのが一般的__ だと思います。

## メソッドに対するデコレーターに関する補足

[前回の記事](https://qiita.com/ysgeso/items/bd63433b7491d8a07ff5#%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AB%E5%AF%BE%E3%81%99%E3%82%8B%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF%E3%83%BC%E5%BC%8F)で、メソッドに対するデコレーター式に、関数で定義したデコレーターを利用できると説明しましたが、__メソッドに対するデコレーター式__ に、__クラスで定義__ した先程の __Showtime を記述__ すると、下記のプログラムのように __エラーが発生__ します。

```python
class Mul:
    @Showtime
    def mul(self, x, y):
        return x * y

mul = Mul()
print(mul.mul(1, 2))        
```

実行結果

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[18], line 7
      4         return x * y
      6 mul = Mul()
----> 7 print(mul.mul(1, 2))

Cell In[9], line 7
      5 def __call__(self, *args, **kwargs):
      6     starttime = datetime.now()
----> 7     retval = self.func(*args, **kwargs)
      8     endtime = datetime.now()
      9     print(endtime - starttime)

TypeError: Mul.mul() missing 1 required positional argument: 'y'
```

このエラーの意味と修正方法を説明すると長くなる点と、__クラスによるデコレーターの定義をあえて使う必要性があまりない__ 点から、今回の記事ではこのエラーの説明は省略します。興味がある方は調べてみると良いでしょう。

# デコレーター式による AI の関数の定義

デコレーター式の使い方を一通り説明したので、__デコレーター式を利用__ して、__AI の関数を定義__ する事にします。

下記は、`ai2s` の定義です。`ai2s` では、ローカル関数として __仮引数 `mb` に代入された局面の評価値を返す__ 処理を行う __`eval_func` を定義__ し、それを __`ai_by_score` の実引数に記述__ して呼び出した結果を返り値として返す処理を行っています。

```python
def ai2s(mb, debug=False, candidate=False):
    def eval_func(mb):
        return 0
        
    return ai_by_score(mb, eval_func, debug=debug, candidate=candidate)
```

この処理は、__局面の評価値を計算__ する処理を行う __`eval_func`__ に対して、 __局面から着手を選択して返す__ ように機能を変更する __ラッパー関数 `ai_by_score` を定義__ していると考えることができます。

なお、今気づいたのですが、`ai2s` の __仮引数 `candidate`__ は、[以前の記事](https://qiita.com/ysgeso/items/2c76474ff139a824f4fd#ai_by_score-%E3%81%AE%E4%BF%AE%E6%AD%A3) で `ai_by_score` を修正した際に __廃止した__ ので、この __`ai2s` を実行するとエラーが発生__ します。そのため、以下の __`ai2s` の修正__ では仮引数 __`candidate` は記述しません__。

## デコレータとしての `ai_by_score` の定義

そこで、__`ai_by_score`__ を下記のプログラムのように __`ai2s` のラッパー関数を返す__ 処理を行う __デコレータ__ として修正します。このデコレーターの定義は[以前の記事](https://qiita.com/ysgeso/items/18ad5c20101c96aaecae#%E9%AB%98%E9%9A%8E%E9%96%A2%E6%95%B0%E3%81%AB%E3%82%88%E3%82%8B%E3%83%A9%E3%83%83%E3%83%91%E3%83%BC%E9%96%A2%E6%95%B0%E3%81%AE%E4%BD%9C%E6%88%90)で説明した高階関数によるラッパー関数 __`create_show_time`__ の作成とほぼ同じなので忘れた方は復習して下さい。

ただし、以前の記事の __`create_show_time`__ は、__任意の関数のラッパー関数を作成__ する処理を行っていたので、作成する __ラッパー関数 `show_time`__ の __仮引数に `*args` と `**kwargs` を記述__ しました。一方、今回は __仮引数として `mb` だけを持つ__ `ai2s` のラッパー関数を定義するので、__ラッパー関数 `wrapper`__ の __仮引数には `mb`__ と、機能を拡張するための __`debug`__、__`rand`__、__`analyze`__ を記述しています。このような方法で定義されたデコレーターは、__汎用ではなく__、__特定の処理を行う関数に対してのみラッパー関数を定義する__ 処理を行います。

また、ラッパー関数に `debug`、`rand`、`analyze` の仮引数を記述することで、__ラップする関数__ の方に __それらの仮引数を記述することなく__、`ai_by_score` によって作成された __すべてのラッパー関数が、それらの仮引数を持つ__ ようになるという利点が得られます。



* __6 行目__：ラップする関数を代入する `eval_func` のみを仮引数とする関数とする
* __7 行目__：[以前の記事](https://qiita.com/ysgeso/items/bd63433b7491d8a07ff5#functoolswrap-%E3%81%AB%E3%82%88%E3%82%8B%E5%95%8F%E9%A1%8C%E3%81%AE%E4%BF%AE%E6%AD%A3)で説明した、`@wraps` のデコレーター式を記述することで、直後で定義する `wrapper` 関数の docstring などの情報をラップする関数の情報に置き換える
* __8 行目__：ラップする関数を定義する。仮引数は元の `ai_by_score` の仮引数からラップする関数を代入する仮引数 `func` を除いたものである。定義する関数の名前は、デコレーターの中のラッパー関数の定義として良く使われる `wrapper` とした。また、この関数の中のプログラムは `ai_by_score` と全く同じである
* __11 行目__：8 行目で定義したラッパー関数を返り値として返す

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

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

```diff_python
from ai import dprint
from functools import wraps
from copy import deepcopy
from random import choice

-def ai_by_score(mb_orig, eval_func, debug=False, rand=True, analyze=False):
+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")
元と同じなので省略
        
+   return wrapper
```
</details>

## デコレーター式を利用した `ai2s` の定義

__`ai_by_score`__ をデコレーター式に記述することで、下記のプログラムのようにデコレーター式の下に、__評価値を計算する処理を行う `eval_func` の定義のみを記述__ して、__`ai2s` を定義__ する事ができます。ただし、作成するラッパー関数の名前を `ai2s` としたいので、定義する関数の名前を `eval_func` ではなく、`ai2s` とする必要があります。

下記のプログラムのように、デコレーター式を利用することで、__評価値を計算する関数を定義するだけ__ で AI の関数を定義できるようになり、以前と比べて __かなりシンプルに記述することができる__ ようになりました。

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

上記の定義後に、下記のプログラムを実行することで、`ai2s` を呼び出すと着手が計算されて返ってくることが確認できます。なお、`ai2s` はランダムな着手を行う AI なので、表示される着手は合法手の中からランダムに選ばれた着手になります。

```python
from marubatsu import Marubatsu

mb = Marubatsu()
print(ai2s(mb))
```

実行結果（実行結果はランダムなので下記と異なる場合があります）

```
(2, 0)
```

また、__ラッパー関数__ `ai2s` は __仮引数 `debug`__、__`rand`__、__`analyze` を持つ__ ので、下記のプログラムのように、__それらを実引数に記述__ して `ai2s` を呼び出すこともできます。

下記はデバッグ表示を行うプログラムです。

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

実行結果

```
Start ai_by_score
Turn o
...
...
...

legal_moves [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
====================
move (0, 0)
Turn x
O..
...
...
score 0 best score -inf
UPDATE
  best score 0
  best moves [(0, 0)]
====================
略
Finished
best score 0
best moves [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
(2, 2)
```

下記は、乱数を使わずに最初の候補手を計算するプログラムで、必ず (0, 0) が返されます。

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

実行結果

```
(0, 0)
```

下記は、候補手の一覧とそれぞれの合法手を着手した局面の評価値を計算して返すプログラムです。

```python
from pprint import pprint

pprint(ai2s(mb, analyze=True))
```

実行結果

```
{'candidate': [(0, 0),
               (1, 0),
               (2, 0),
               (0, 1),
               (1, 1),
               (2, 1),
               (0, 2),
               (1, 2),
               (2, 2)],
 'score_by_move': {(0, 0): 0,
                   (0, 1): 0,
                   (0, 2): 0,
                   (1, 0): 0,
                   (1, 1): 0,
                   (1, 2): 0,
                   (2, 0): 0,
                   (2, 1): 0,
                   (2, 2): 0}}
```

これで、`ai2s` をデコレーター式で定義する事ができましたが、実はこのままでは `ai14s` のように、`mb` 以外の仮引数を持つ AI の関数に対して `ai_by_score` を利用することはできません。次回の記事ではその点について説明します。

なお、`ai_by_score` を修正したため、`ai2s` 以外の `ai_by_score` を利用する AI は正しく動作しなくなっています。それらについても次回の記事で修正します。

# 今回の記事のまとめ

今回の記事では、クラスによるデコレーターの定義の方法、クロージャーによるオブジェクト指向の記述、デコレーターによる `ai2s` の定義の方法について説明しました。

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

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

# 次回の記事

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

[^1]:直前で `add` という名前のローカル変数に Add クラスのインスタンスを代入したので、それと区別するために `add_func` という名前にしました
[^2]:そのことは、[本家のドキュメント](https://docs.python.org/ja/3/tutorial/classes.html#method-objects)に `x` が MyClass のインスタンスの場合は「`x.f()` という呼び出しは、`MyClass.f(x)` と厳密に等価なものです」と記述されています
[^3]:`class Showtime(func):` のように記述すると、`func` は仮引数ではなく、[以前の記事](https://qiita.com/ysgeso/items/2acd8c5ceb8e9cda7233#%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E7%B6%99%E6%89%BF%E3%81%AE%E8%A8%98%E8%BF%B0%E6%96%B9%E6%B3%95)で説明した __クラスの継承__ の __基底クラス__ という __全く異なる意味__ になります