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を一から作成する その130 クラスによるデコレーターの定義とデコレーター式による AI の定義

Last updated at Posted at 2024-11-04

目次と前回の記事

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

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

リンク 説明
marubatsu.py Marubatsu、Marubatsu_GUI クラスの定義
ai.py AI に関する関数
test.py テストに関する関数
util.py ユーティリティ関数の定義。現在は gui_play のみ定義されている
tree.py ゲーム木に関する Node、Mbtree クラスの定義
gui.py GUI に関する処理を行う基底クラスとなる GUI クラスの定義

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

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

前回の記事で、デコレーター式の種類 について説明を行いましたので、その続きを説明します。前回の記事では、デコレーター式 によるラッパー関数の定義では @ の直後に下記のような性質を持つデコレーター関数名を記述 すると説明しました。

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

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

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

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

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

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

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

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

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

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

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

実行結果

3

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

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

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

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

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

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

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

公式のドキュメントには、x という呼び出し可能オブジェクトに対して、 x(arg1, arg2, ...) のような呼び出しを行った場合は、おおまかに(roughly)type(x).__call__(x, arg1, arg2, ...) と同じ処理を行うと説明されています。この説明について解説します。

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

組み込み関数 type

type は、オブジェクトのデータ型を返す 処理を行う組み込み関数で、一般的にはその オブジェクトを作成したクラス が得られます。例えば、先程 Add クラスから作成 したインスタンスを代入した add に対して type(add) を実行すると、実行結果のように Add クラスが得られたことが確認 できます。なお、実行結果に表示される __main__.Add は、Add クラスが メインモジュールで定義されている ことを表します。JupyterLab に記述して実行 したプログラムは、以前の記事で説明したように、メインモジュールのプログラム です。

print(type(add))

実行結果

<class '__main__.Add'>

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

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

print(type(add_func))

実行結果

<class 'function'>

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

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

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

以前の記事で説明したように、クラス 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, ...) と同じ処理を行う」という記述と、上記のことから、以下の事がわかります。

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

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

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

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

print(add.__call__(1, 2))

実行結果

3

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

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

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

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 の関数の定義を再掲します。

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 行目の クロージャー変数 func が、インスタンスの属性の self.func になる点が異なる
  • create_show_time では、定義した ラッパー関数を return show_time で返す 処理を行っていたが、クラスから作成された インスタンスがラッパー関数の役割を果たす ため、その処理は必要がなくなったので削除 する
 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
行番号のないプログラム
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
修正箇所
-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

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

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

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

print(add_1(1))

実行結果

0:00:00
2

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

前回の記事で説明したように、下記の 1 ~ 3 行目の デコレーター式が行う処理 は、5 ~ 7 行目と同じ 処理を行います。そのことは、デコレーター式に記述した名前が関数であっても、クラスであっても変わることはありません。

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__ メソッドが呼び出されます

print(add_1(1))

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

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

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

関数 クラス
行う処理 ラッパー関数 を定義して返す ラッパー関数と同様の機能 を持つ、呼び出し可能なクラスの インスタンスを作成 する
ラップする関数の記録 クロージャー変数
デコレーターのローカル名前空間に記録
インスタンスの属性
インスタンスの名前空間に記録
ラッパー関数の定義 ローカル関数 で定義 __call__ メソッド で定義

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

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

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

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

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

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

2 つの方法の違いは、作成するラッパー関数が「関数オブジェクト」であるか「クラスのインスタンス」であるかと、ラップする関数の記録を クロージャー の仕組みを利用した「クロージャー変数」に記録するか、「インスタンスの属性」に記録するかですが、どちらも行っている処理は上記のように本質的には変わりません。

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

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

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 のインスタンスを作成し、printxprinty メソッドを呼び出すと、下記のような実行結果が表示されます。

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

実行結果

1
2

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

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 つの関数を呼び出すと、下記のように先程と同様の実行結果が表示されます。

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

実行結果

1
2

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

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

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

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

ローカル変数との区別

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

代入処理の違い

インスタンスの属性は、メソッドの処理の中で参照と代入のどちらも行うことができますが、クロージャー変数 は、クロージャーの関数呼び出しの処理の中で参照を行うことはできますが、そのままでは代入処理を行うことはできません。その理由は、以前の記事で説明したように、名前解決のルール から、クロージャー変数と同じ名前の変数に代入 を行うと、ローカル変数に対する代入処理になる からです。

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

クロージャー変数のこの性質は、関数の処理の中で グローバル変数に代入処理を行えない 点と似ています。また、nonlocal以前の記事で説明した、関数の中でグローバル変数に対する代入処理を行う際に、global グローバル変数名 を記述する必要があるのと同じです。

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

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

クロージャー は、それ自体が関数オブジェクト なので、呼び出すことができる処理 は基本的には 1 種類 しかありません。その代わり、今回の記事の最初で定義した Add クラスadd_func のように、呼び出す処理が 1 種類しかない 場合は、クロージャーの関数の定義 のほうが、クラスより 簡潔に記述できる という利点があります。

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

両者の使い分け

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

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

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

前回の記事で、メソッドに対するデコレーター式に、関数で定義したデコレーターを利用できると説明しましたが、メソッドに対するデコレーター式 に、クラスで定義 した先程の Showtime を記述 すると、下記のプログラムのように エラーが発生 します。

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 の実引数に記述 して呼び出した結果を返り値として返す処理を行っています。

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 は、以前の記事ai_by_score を修正した際に 廃止した ので、この ai2s を実行するとエラーが発生 します。そのため、以下の ai2s の修正 では仮引数 candidate は記述しません

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

そこで、ai_by_score を下記のプログラムのように ai2s のラッパー関数を返す 処理を行う デコレータ として修正します。このデコレーターの定義は以前の記事で説明した高階関数によるラッパー関数 create_show_time の作成とほぼ同じなので忘れた方は復習して下さい。

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

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

  • 6 行目:ラップする関数を代入する eval_func のみを仮引数とする関数とする
  • 7 行目以前の記事で説明した、@wraps のデコレーター式を記述することで、直後で定義する wrapper 関数の docstring などの情報をラップする関数の情報に置き換える
  • 8 行目:ラップする関数を定義する。仮引数は元の ai_by_score の仮引数からラップする関数を代入する仮引数 func を除いたものである。定義する関数の名前は、デコレーターの中のラッパー関数の定義として良く使われる wrapper とした。また、この関数の中のプログラムは ai_by_score と全く同じである
  • 11 行目:8 行目で定義したラッパー関数を返り値として返す
 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
行番号のないプログラム
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
修正箇所
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

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

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

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

@ai_by_score
def ai2s(mb):
    return 0

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

from marubatsu import Marubatsu

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

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

(2, 0)

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

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

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) が返されます。

print(ai2s(mb, rand=False))

実行結果

(0, 0)

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

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 本記事で入力して実行した JupyterLab のファイル
ai.py 本記事で更新した ai_new.py

次回の記事

近日公開予定です

  1. 直前で add という名前のローカル変数に Add クラスのインスタンスを代入したので、それと区別するために add_func という名前にしました

  2. そのことは、本家のドキュメントx が MyClass のインスタンスの場合は「x.f() という呼び出しは、MyClass.f(x) と厳密に等価なものです」と記述されています

  3. class Showtime(func): のように記述すると、func は仮引数ではなく、以前の記事で説明した クラスの継承基底クラス という 全く異なる意味 になります

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?