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を一から作成する その127 ラッパー関数

Last updated at Posted at 2024-10-24

目次と前回の記事

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

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

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

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

今回の記事の内容

前回の記事では Marubatsu_GUI クラスを、「ゲーム盤のマスに、そのマスに着手を行った際の AI の評価値を表示できる」ように改良するために、ai3ai3s を下記のように修正しました。

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

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

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

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

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

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

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

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

f()

実行結果

関数 f を実行しました

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

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

関数の処理時間 は、以前の記事で説明した、datetime モジュールの datetime.now という 現在時刻を計算する関数 を利用することで計算できます。具体的には、datetime.now() で計算した現在時刻のデータを - 演算子で演算する ことで、2 つの時刻の差 を計算することができます。

  • 4 行目:関数 f のブロックの先頭でその時点の時刻を計算し、starttime に代入する
  • 6、7 行目:関数 f の末尾で、その時点の時刻を計算して endtime に代入し、starttime との差を計算して表示することで、処理時間を表示する
1  from datetime import datetime
2
3  def f():
4      starttime = datetime.now()
5      print("関数 f を実行しました")
6      endtime = datetime.now()
7      print(endtime - starttime)
行番号のないプログラム
from datetime import datetime

def f():
    starttime = datetime.now()
    print("関数 f を実行しました")
    endtime = datetime.now()
    print(endtime - starttime)
修正箇所
from datetime import datetime

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

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

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

f()

実行結果

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

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

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)
行番号のないプログラム
def f():
    starttime = datetime.now()
    c = 0
    for i in range(10000):
        c += i
    print("関数 f を実行しました")
    endtime = datetime.now()
    print(endtime - starttime)
修正箇所
def f():
    starttime = datetime.now()
+   c = 0
+   for i in range(10000):
+       c += i
    print("関数 f を実行しました")
    endtime = datetime.now()
    print(endtime - starttime)
f()

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

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

この方法の欠点

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

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

 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 を拡張前の関数に戻します。

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

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

1  def f_show_time():
2      starttime = datetime.now()
3      f()
4      endtime = datetime.now()
5      print(endtime - starttime)
行番号のないプログラム
def f_show_time():
    starttime = datetime.now()
    f()
    endtime = datetime.now()
    print(endtime - starttime)

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

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

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

f_show_time()

実行結果

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

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

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

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

g_show_time()

実行結果

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

ラッパー関数の性質

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

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

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

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

一方、ai.py で定義した下記の dprint は仮引数 debug の値が True の場合のみ print で表示を行うという、print という関数の機能を変化する目的で定義 されているので、ラッパー関数です

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

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

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

ラッパー関数の主な用途

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

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

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

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

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

具体例としては、以前の記事で説明した、再帰呼び出しによる繰り返し処理 が挙げられます。下記は、以前の記事で定義した、ボトムアップな再帰呼び出しによって 0 から n までの整数の合計を計算する関数です。

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

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

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

実行結果

55

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

def sum_by_br_from_0(n):
    return sum_by_br(n, i=0, total=0)
print(sum_by_br_from_0(10))

実行結果

55

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

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

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

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

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

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

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

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 メソッドの定義の先頭の行の部分です。

def create_tree_by_df(self, node):

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

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

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

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 メソッドについて忘れた方は、以前の記事 を復習して下さい。

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

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

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 を呼び出す ことができるようになります。

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" という文字列の座標に変換されます。

print(exchange_xy_by_tuple((1, 2)))

実行結果

2,1

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

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

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

  • 5 行目:返り値に対して split メソッドを利用することで、x 座標と y 座標の値を取り出して xy に代入する
  • 6 行目:座標を表す tuple を計算して retval_tuple に代入する。なお、split メソッドで作成した list の 要素は文字列型のデータ なので、組み込み関数 float を使って float 型のデータに 型変換を行う必要がある 点に注意すること
  • 7 行目:tuple に変換した座標データを返り値として返す
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
行番号のないプログラム
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
修正箇所
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

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

print(exchange_xy_by_tuple((1, 2)))

実行結果

(2.0, 1.0)

単位を変換する例

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

def f_minitue()
    return f() / 60

機能の変更

先程説明したように、ラッパー関数を定義する事で、ラップした関数に 機能の追加や変更 を行うことができます。先ほどは処理時間を表示する機能の追加例を紹介しましたが、ここでは 機能の変更例 として、反復可能オブジェクトの要素の合計を計算 する組み込み関数 sum に、角度の合計を計算する ように 機能を変更 する ラッパ関数を定義 します。

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

print(sum([200, 300]))

実行結果

500

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

  • 1 行目sum_degree の仮引数の値を、sum を呼び出す際に実引数として記述できるように、仮引数 *args**kwargs を記述する。この部分の意味を忘れた方は以前の記事を復習すること
  • 2 行目sum_degree の仮引数の値を実引数に記述して sum を呼び出し、返り値を degree に代入する
  • 3 行目degree の値を 0 以上 360 未満の値に変換するために、% 演算子で 360 で割った余りを計算する
  • 4 行目degree の値を返り値として返す
1  def sum_degree(*args, **kwargs):
2      degree = sum(*args, **kwargs)
3      degree %= 360
4      return degree
行番号のないプログラム
def sum_degree(*args, **kwargs):
    degree = sum(*args, **kwargs)
    degree %= 360
    return degree

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

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

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

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

実行結果

200
200

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

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

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

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

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

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

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

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

  • 1 行目:処理時間を計測する関数を代入する仮引数 func を追加する
  • 3 行目func を呼び出すように修正する
1  def show_time(func):
2      starttime = datetime.now()
3      func()
4      endtime = datetime.now()
5      print(endtime - starttime)
行番号のないプログラム
def show_time(func):
    starttime = datetime.now()
    func()
    endtime = datetime.now()
    print(endtime - starttime)
修正箇所
-def f_show_time(func):
+def show_time(func):
    starttime = datetime.now()
-   f()
+   func()
    endtime = datetime.now()
    print(endtime - starttime)

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

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

show_time(f)

実行結果

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

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

show_time(g)

実行結果

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

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

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

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

  • 1 行目:仮引数 *args**kwargs を追加する
  • 3 行目:実引数に *args**kwargs を記述して func を呼び出す。また、その 返り値を retval に代入 する。これは、4、5 行目で行う func を呼び出した後の 後処理を行った後で返り値を返す ことができるようにするためである
  • 6 行目:関数の最後の処理で、retval を返り値として返す
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
行番号のないプログラム
def show_time(func, *args, **kwargs):
    starttime = datetime.now()
    retval = func(*args, **kwargs)
    endtime = datetime.now()
    print(endtime - starttime)
    return retval
修正箇所
-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

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

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

実行結果

0:00:00
300

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

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

実行結果

0:00:00
100

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

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

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

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

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

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 を呼び出して 行うことができる ことからも、ai3sai_by_score のラッパー関数 であると言えるでしょう。実際に実行結果のようにどちらも同じ返り値を返します。

from ai import ai3
from marubatsu import Marubatsu

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

実行結果

(1, 1)
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 本記事で入力して実行した JupyterLab のファイル

次回の記事

  1. ラッパー関数の厳密な定義は決まっていないと思います。そのため、文献や人によってはラッパー関数を若干異なる意味で使われていると思います

  2. 再帰呼び出し(recursive call)の頭文字を表す r です

  3. (x, y)"x,y" の x と y には、それぞれ x 座標と y 座標を表す数字が入ります

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?