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を一から作成する その129 デコレーターとデコレーター式によるラッパー関数の定義

Last updated at Posted at 2024-11-01

目次と前回の記事

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

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

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

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

デコレータ式によるラッパー関数の定義

前回の記事では、下記のプログラムのような、ラップする関数を代入する仮引数 を持ち、ラッパー関数を定義して返す 処理を行う 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

また、下記のプログラムのように、create_show_time を利用して sum 関数に処理時間を表示する機能を追加した ラッパー関数を作成 して sum_show_time に代入 しました。

sum_show_time = create_show_time(sum)
print(sum_show_time([10, 20]))

実行結果

0:00:00
30

デコレータとは何か

create_show_time のような、ラップする関数を仮引数に代入し、ラッパー関数を定義して返すような高階関数の事を、Python では デコレーター(decorator) と呼びます。ラップする関数の 機能を拡張 して 飾り付けを行う(decorate) 処理を行うことから、デコレーターと名付けられたようです。

なお、デコレーターという、特別な種類の関数があるわけではありません。デコレーター はそのような処理を行う 通常の関数の一種 にすぎません。

上記では、デコレータのことを関数と説明しましたが(下記のリンク先にもそのように説明されています)、関数以外にもデコレーターの機能を持つクラスを定義する事もできます。その方法については次回の記事で説明します。

また、関数以外にも、クラスの機能を拡張した、ラッパークラスを定義するデコレーターを作成することもできます。そちらについては本記事で言及します。

デコレーターの用語の詳細については、下記のリンク先を参照して下さい。

ラッパー関数の名前

上記の例では、sum 関数のラッパー関数に sum_show_time という名前を付けましたが、
create_show_time によって作成されるラッパー関数は、ラップする関数と 同じ仮引数同じ返り値 を持ち、元の機能を保持したまま機能を拡張 するという処理を行います。

このような場合は、ラッパー関数に ラップする関数と同じ名前を付ける ことが良くあります。具体的には、下記のプログラムの 1 行目のように、sum_show_time によって作成された sum のラッパー関数 の返り値を、sum という名前の変数に代入 します。

このような処理を行うことで、2 行目のように、ラップする関数を使ったプログラムの 記述を全く変えることなく、機能を拡張したラッパー関数を使った処理が行われるようになるという利点が得られます。

sum = create_show_time(sum)
print(sum([10, 20]))

実行結果

0:00:00
30

なお、この方法には ラップする関数を利用できなくなる という欠点があります。上記の例の場合は、元の組み込み関数 sum の処理を 利用することができなくなります。ラップする関数を 引き続き利用したい場合 は、ラッパー関数の名前をラップする関数と 異なる名前を付ける 必要があります。

デコレーター式によるラッパー関数の定義

デコレータを利用して、ラップする関数と 同じ名前のラッパー関数を定義 するという処理は、比較的よく行われます。そこで、そのようなラッパー関数を デコレーター式 という構文で 簡潔に定義 する方法が用意されています。

デコレーター式は @ で始まる構文 で、デコレーター式を利用した ラッパー関数の定義 は下記のように記述します。これまでのプログラムで クラスメソッド静的メソッド を定義する際に、メソッドの定義の直前の行 に記述した @classmethod@staticmethod は Python で良く利用されるデコレータ式です。

@デコレータの処理を行う関数名
ラップする関数の定義  

@ の後ろには、以下のような性質を持つデコレータの処理を行う関数名を記述します。前回の記事で定義した create_show_time などが該当します。

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

デコレーター式のすぐ下 には、ラップする関数の定義をそのまま記述 します。例えば、下記は、仮引数 x に代入された値に 1 を足した数を返す add_1 という関数に対して、デコレーター式@create_show_time を記述して、処理時間を計算して表示する機能を追加した add_1 という名前のラッパー関数を定義 するプログラムです。

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

上記を実行すると、add_1 のラッパー関数が作成 され、同じ add_1 という名前の変数に代入 されます。そのため、add_1 を呼び出す と、下記のプログラムのように 処理時間が表示される ようになります。

print(add_1(1))

実行結果

0:00:00
2

デコレーター式が行う処理

下記の 1 ~ 3 行目の デコレーター式と関数の定義 を実行した際に 行われる処理 は、下記の 5 ~ 7 行目のプログラムで行われる処理と同じ です。

1  @decorator
2  def func(*args, **kwargs): # 関数名や仮引数は何でもよい
3      pass                   # ここで行う処理は何でもよい
4
5  def func(*args, **kwargs): # デコレータ式の下に記述した関数の定義と同じ
6      pass                  
7  func = decorator(func)

そのため、下記の 1 ~ 3 行目の デコレーター式と関数の定義を実行 すると、下記 5 ~ 7 行目のプログラムと同じ処理 が行われされます。5 ~ 7 行目で行われる処理は、先程説明したデコレータである create_show_time を呼び出して add_1 のラッパー関数を定義 して、add_1 に代入 する処理です。

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

このように、デコレータ式は、別の方法で記述 できるプログラムを、より簡潔に記述するため導入された構文 です。このようなプログラムを 書きやすくわかりやすく記述できるように導入 された構文のことを シンタックスシュガー(syntax sugar、糖衣構文) と呼びます。Python のシンタックスシュガーには、他にも list 内包表記などがあります。

なお、シンタックスシュガーは慣れれば便利ですが、プログラム言語に特有 のものが多く、他のプログラミング言語では利用できない場合が多い 点に注意する必要があります。そのため、例えば Python 以外のプログラミングを学んだ人にとっては list 内包表記や、デコレーター式を見てもすぐに意味が理解できない可能性が高いでしょう。また、Python で記述したプログラムを 別のプログラム言語に書き直す 必要がある場合では、シンタックスシュガーの記述の書き直しの作業が問題となる 場合があります。

デコレーター式の詳細については下記のリンク先を参照して下さい。

デコレーター式が行う処理の補足

厳密には下記のプログラムの 1 ~ 3 行目のデコレーター式が行う処理は、4 ~ 6 行目で行われる処理とは ほんの少しだけ異なります。そのことは、上記のリンク先では「だいたい次と等価です」と、「前者のコード(デコレーター式のプログラムの事です)では元々の関数を func という名前へ一時的に束縛することはない」のように説明されています。

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

デコレーター式が行う処理を理解 する際に、その違いを意識する必要はありません が、その違いを知りたい人がいるかもしれないので以下に説明します。下記の説明の意味がわからない方は読み飛ばしても構いません。

「元々の関数を func という名前へ 一時的に束縛 する」という説明は、上記の 5 ~ 7 行目で行われる下記の処理の事を意味しています。

  • 5、6 行目の 関数の定義を実行 することで グローバル名前空間 に、add_1 という名前と作成された関数オブジェクトの対応づけが登録される。このような 名前とオブジェクトの対応づけの名前空間への登録 のことを オブジェクトを名前に束縛する(bound)と呼ぶ ので、この処理によって 5、6 行目に記述されている 元々の関数が add_1 という名前に束縛 されたことになる
  • 7 行目の処理によって、add_1 にラッパー関数が代入される。その結果、元々の関数の add_1 への束縛が解除 される。このように、add_1 への元々の関数の束縛が すぐに解除される ことを、上記の説明では「一時的に束縛する」と表記している

5、6 行目で行われる元々の関数を add_1 という名前に 一時的に束縛するという処理 は、add_1 という名前のラッパー関数を定義 する際に行う必要がない 無駄な処理 です。上記の 1 ~ 3 行目の デコレーター式が行う処理 では、その 無駄な処理を行わない という点で、5 ~ 7 行目が行う処理と異なります。それが、「前者のコード(デコレーター式のプログラム)では元々の関数を func という名前へ一時的に束縛することはない」という説明の意味です。

なお、この無駄な処理が行われるかどうかによって、最終的な処理の結果は変わらない ので、上記の 違いを意識する必要はありません

デコレーター式に関するエラー

先程説明した、下記の条件を満たさない関数 をデコレーター式に記述すると エラーが発生する ので、それらのエラーについて簡単に説明します。

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

仮引数が存在しない場合

下記のプログラムの decorator_error1 は、仮引数を一つも持たない ので、デコレーター式を実行すると実行結果のように decorator_error1 には位置引数がない(0 positional argument)にも関わらず実引数が記述されているという意味のエラーが発生します。

def decorator_error1():
    pass

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

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[5], line 4
      1 def decorator_error1():
      2     pass
----> 4 @decorator_error1
      5 def add_1(x):
      6     return x + 1

TypeError: decorator_error1() takes 0 positional arguments but 1 was given

これは、上記のプログラムは、下記のプログラムと同じ処理を行うからです。

def decorator_error1():
    pass
    
def add_1(x):
    return x + 1
add_1 = decorator_error1(add_1)

仮引数が 2 つ以上存在する場合

下記のプログラムの decorator_error2 は、仮引数を 2 つ持つ ので、デコレーター式を実行すると実行結果のように、decorator_error2 を呼び出す際に仮引数 g に対応する位置引数が記述されていないという意味のエラーが発生します。

def decorator_error2(f, g):
    pass

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

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[6], line 4
      1 def decorator_error2(f, g):
      2     pass
----> 4 @decorator_error2
      5 def add_1(x):
      6     return x + 1

TypeError: decorator_error2() missing 1 required positional argument: 'g'

返り値が関数でない場合

下記のプログラムの decorator_error3 は、仮引数を 1 つ持つ点では問題はありませんが、返り値が関数でない2 点が問題です。ただし、下記のプログラムを実行しても その時点ではエラーは発生しません

def decorator_error3(func):
    return 1

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

エラーが発生するのは、下記のプログラムのように add_1 を呼び出した場合です。

print(add_1(1))

実行結果

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 print(add_1(1))

TypeError: 'int' object is not callable

add_1 には、 decorator_error3(add_1) の返り値が代入 されますが、decorator_error3常に 1 を返す ので、add_1 には 1 が代入 されています。そのため、整数型(int)である 1 は関数のように呼び出すことはできない(is not callable)というエラーが発生します。また、実際に下記のプログラムを実行すると、add_11 が代入 されていることが確認できます。

print(add_1)

実行結果

1

デコレータ式の使用上の注意

デコレーターは便利ですが、使用する際には いくつか気を付ける必要 があるので、それらについて説明します。

ラップする関数を利用できなくなる

デコレータ式は、ラップする関数と 同じ名前のラッパー関数を定義 するので、ラップする関数を利用できなくなってしまう という欠点があります。ラップする関数を そのままの形で利用し続ける必要がある 場合は、デコレーター式を利用することはできず、下記のプログラムの 3 行目のような方法で、ラップする関数と 異なる名前の変数 に作成したラッパー関数を代入する必要があります。

def add_1(x):
    return x + 1
add_1_show_time = create_show_time(add_1)

定義済の関数に対して利用できない

デコレータ式は 直後の行ラップする関数の定義を記述 する必要があるので、既に 定義済の関数 に対して 利用することはできません。例えば、sum のような 組み込み関数 や、他のモジュールで定義された関数 に対してデコレーター式を利用することはできません。

docstring などの表示がラッパー関数のものになる

デコレーター式によって定義された ラッパー関数 は、名前はラップする関数と同じ になりますが、その 実態 はデコレーターの関数の中で定義された ラッパー関数 です。

そのため、ラッパー関数の docstring を表示した場合に、ラップする関数 に記述した docstring ではなく、ラッパー関数の docstring が表示 されるなどの 問題が生じます

デコレーター式を実行する前の処理

具体例を挙げて説明します。

まず、add_1 を、下記のプログラムのように docstring を記述したプログラム として 再定義 します。

def add_1(x):
    """1 を足して返す
    
    Args:
        x: 加算する値
    
    Returns:
        x + 1 の値
    """

    return x + 1

関数の定義に記述した docstring組み込み関数 help を使って表示 することができ、add_1 の docstring は下記のプログラムで表示できます。

help(add_1)

実行結果

Help on function add_1 in module __main__:

add_1(x)
    1 を足して返す
    
    Args:
        x: 加算する値
    
    Returns:
        x + 1 の値

また、関数が定義された時def 文で付けられた名前 は、関数オブジェクトの __name__ という 特殊属性に代入 されています。そのため、下記のプログラムで add_1__name__ 属性を表示すると、実行結果のように add_1 が表示されます。

print(add_1.__name__)

実行結果

add_1

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

関数オブジェクトの特殊属性 __name__ の詳細については下記のリンク先を参照して下さい。

デコレーター式を実行した後の処理

次に、デコレーター式を実行した後 で、上記と同様の処理を行うと、どのように結果が変わるかを説明します。まず、デコレーターである create_show_time を、下記のプログラムのように ラッパー関数 show_time に対して docstring を記述 したプログラムとして 再定義 します。なお、デコレーターの中で定義する ラッパー関数の名前 は、wrapper_wrapper という 名前にすることが多い ようですが、本記事ではこれまでと同様に show_time のままとしました。

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

上記を実行後に、下記のプログラムで @create_show_time のデコレーター式を使って add_1 に対するラッパー関数を定義します。

@create_show_time
def add_1(x):
    """1 を足して返す
    
    Args:
        x: 加算する値
    
    Returns:
        x + 1 の値
    """
    
    return x + 1

上記のプログラムを実行すると、add_1 という名前の変数 に、create_show_time で定義された ラッパー関数 show_time が代入 されます。その結果、add_1 には上記のプログラムに記述した ラップする add_1 の定義とは異なる関数が代入 されることになります。

そのため、下記のプログラムで help を使って add_1 の docstring を表示すると、実行結果のように show_time の docstring が表示 されてしまいます。

help(add_1)

実行結果

Help on function show_time in module __main__:

show_time(*args, **kwargs)
    ラップする関数の処理時間を表示するラッパー関数

同様に add_1__name__ 属性を表示すると、実行結果のように show_time が表示されます。

print(add_1.__name__)

実行結果

show_time

functools.wrap による問題の修正

create_show_time のように、ラップする関数 の仮引数、返り値、処理に 変更を加えずちょっとした機能の追加 を行うようなラッパー関数を定義する場合は、docstring や、__name__ 属性の値 は、ラッパー関数ではなく、ラップする関数の情報が表示された方が便利 です。実際に上記の help(add_1) の実行で、show_time の docstring が表示されても add_1使い方を知ることはできません

この問題は、ラッパー関数 に対して functools モジュールで定義された wraps によるデコレーター式を記述 することで修正することできます。

wraps をデコレーター式に記述する際は、下記のプログラムの 4 行目のように ラップする関数を実引数に記述して呼び出す という形で記述します。

@wraps(func) は、その下に定義した関数の docstring や __name__ 属性の値などを、実引数 func で指定した関数の情報に置き換える という機能を持つデコレーター式です。

 1  from functools import wraps
 2
 3  def create_show_time(func):
 4      @wraps(func)
 5      def show_time(*args, **kwargs):
 6          """ラップする関数の処理時間を表示するラッパー関数"""
 7       
 8          starttime = datetime.now()
 9          retval = func(*args, **kwargs)
10          endtime = datetime.now()
11          print(endtime - starttime)
12          return retval
13    
14      return show_time
行番号のないプログラム
from functools import wraps

def create_show_time(func):
    @wraps(func)
    def show_time(*args, **kwargs):
        """ラップする関数の処理時間を表示するラッパー関数"""
        
        starttime = datetime.now()
        retval = func(*args, **kwargs)
        endtime = datetime.now()
        print(endtime - starttime)
        return retval
    
    return show_time
修正箇所
+from functools import wrap

def create_show_time(func):
+   @wrap(func)
    def show_time(*args, **kwargs):
        """ラップする関数の処理時間を表示するラッパー関数"""
        
        starttime = datetime.now()
        retval = func(*args, **kwargs)
        endtime = datetime.now()
        print(endtime - starttime)
        return retval
    
    return show_time

functools は、高階関数に対する様々な処理を行うためのモジュールです。functools の wraps の詳細については、下記のリンク先を参照して下さい。

上記の修正後に、改めて下記のプログラムで add_1 に対して修正した create_show_time を利用してラッパー関数を作成します。

@create_show_time
def add_1(x):
    """1 を足して返す
    
    Args:
        x: 加算する値
    
    Returns:
        x + 1 の値
    """
    
    return x + 1

上記の実行後に下記のプログラムで help を使って add_1 の docstring を表示すると、実行結果のように、再び add_1 の docstring が表示される ようになります。

help(add_1)

実行結果

Help on function add_1 in module __main__:

add_1(x)
    1 を足して返す
    
    Args:
        x: 加算する値
    
    Returns:
        x + 1 の値

同様に add_1__name__ 属性を表示すると、実行結果のように 再び add_1 が表示される ようになります。

print(add_1.__name__)

実行結果

add_1

ラッパー関数が、ラップする関数と 同じような機能を持つ場合 は、functools モジュールの wraps をインポートし、デコレーターの中で定義されたラッパー関数に対して @wraps(func) のデコレータ式を記述すると良い

ラップする関数と無関係な処理を行うデコレーターをデコレーター式に記述できてしまう

デコレーター式は 一般的 には ラップする関数の機能を拡張 したラッパー関数を定義するために利用されますが、下記のプログラムの 1 ~ 3 行目のデコレーター式によるラッパー関数を作成する処理は、5 ~ 7 行目の処理を 別の方法で記述しているにすぎません

そのため、下記の 5 ~ 7 行目 のプログラムを実行した際に エラーが発生しなければdecoratorどのような処理を行う関数であっても、1 ~ 3 行目のプログラムを実行することで 2、3 行目の func のラッパー関数を定義できてしまいます。

1  @decorator
2  def func(*args, **kwargs): # 関数名や仮引数は何でもよい
3      pass                   # ここで行う処理は何でもよい
4
5  def func(*args, **kwargs): # デコレータ式の下に記述した関数の定義と同じ
6      pass                  
7  func = decorator(func)

具体的には、先程説明したように、以下の条件を満たす関数であれば、デコレーター式とその下の関数の定義を実行しても エラーは発生しません

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

例えば、下記の decorator_sum は、仮引数 func を 1 つだけ持ち、組み込み関数 sum を返す ので、その中で ラッパー関数を定義していません が、上記の条件を満たします。

def decorator_sum(func):
    return sum

上記の定義後に、下記のプログラムのように decorator_sum をデコレーター式に記述して、add_1 のラッパー関数を定義 する処理を実行してもエラーは発生しません。

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

上記のプログラムは、下記のプログラムと同じ処理を行います。

add_1 = decorator_sum(add_1)

decorator_sum は、仮引数に代入された関数 とは 全く無関係 に、常に sum という組み込み関数を返す という処理を行うので、decorator_sum(add_1) の返り値は sum になります。従って、上記のプログラムは、下記のプログラムと同じ処理が行われます。

add_1 = sum

上記のことから、デコレータ式に @decorator_sum を記述 すると、その下に定義した関数にどのような処理が記述されていても、必ず sum と同じ処理を行うラッパー関数が定義される ことになります。実際に、下記のプログラムのように、add_1([10, 20]) を実行すると、sum([10, 20]) と同じ処理が行われ、実行結果のように 30 が表示されます。

add_1([10, 20])

実行結果

30

一般的 には、上記のような ラップする関数と完全に無関係な処理を行う ような関数をデコレーター式に 記述することはありません が、上記のようなことができてしまうということは頭の片隅にでも入れておいたほうが良いでしょう。

実引数と返り値がそのまま使われない場合がある

ラッパー関数の中で、ラッパー関数の 仮引数に代入された値を全く利用せずラップする関数を呼び出す ようなデコレーターを定義する事ができます。

例えば、下記のプログラムの decorator_arg10 は、下記のような処理を行うラッパー関数を返します。

  • ラップする関数 func実引数に必ず 10 を記述して呼び出し、その返り値を返す処理を行う
  • その際に、ラッパー関数 wrapper仮引数 argskwargs の値を一切利用しない
def decorator_arg10(func):
    def wrapper(*args, **kwargs):
        return func(10)
    return wrapper

その結果、下記のプログラムのように、decorator_10 をデコレーター式に記述して add_1ラッパー関数を定義 すると、ラッパー関数の 実引数 にどのような値を記述して呼び出してもその 値は無視され、ラップする元の関数の add_1実引数に常に 10 が記述 して呼び出された場合の処理が行われます。

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

実際に、下記のプログラムを実行すると、実行結果では add_1実引数の値に関わらず常に 11 が表示 されます。

print(add_1(1))
print(add_1(100))

実行結果

11
11

同様に、下記のプログラムのようなデコレータを定義してデコレーター式に記述すると、実引数にどのような値を記述しても 必ず 10 が返る ようなラッパー関数が作成されます。

def decorator_ret10(func):
    def wrapper(*args, **kwargs):
        return 10
    return wrapper

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

print(add_1(1))
print(add_1(100))

実行結果

10
10

上記のような、実引数を完全に無視 したり、返り値を特定の値にしてしまう ようなデコレータを 定義して利用することはほぼない と思いますが、こちらもこのようなことができてしまうことは頭の片隅にでも入れておいたほうが良いでしょう。

なお、以前の記事で紹介した関数呼び出しの記述の簡略化や、データー構造の変更のように、ラップする関数の 仮引数の一部に特定の値を代入 にしたり、返り値に対して何らかの計算を行って返す ようなラッパー関数を定義する場合は 実際にあります。先ほど functools.wraps を紹介しましたが、そのような場合は、ラッパー関数の定義の前に functools.wraps を記述しても、ラップする関数の docstring の内容 が、ラッパー関数で行う処理と大きく異なってしまう ことになるので、あまり有用ではないかもしれません

なお、この点に関する筆者のデコレーターに対する知識がまだ曖昧なので、詳しい方はコメントで補足して頂ければ助かります。

ここまでの内容をまとめると以下のようになります。

デコレーター式 によるラッパー関数の定義の多くは以下のようなものが多い。そのような場合は、デコレーターの中でラッパー関数を定義 する際に、@wraps(func) のデコレータ式を記述すると良い。

  • ラップする関数の仮引数をそのまま利用する
  • ラップする関数の返り値をそのまま返す
  • 行う機能の拡張はラップする関数の処理を大きく変えるものではない

ただし、上記の条件を満たさないようなデコレーター式を記述してラッパー関数を定義する場合もある点に注意が必要である。

複数のデコレーター式の記述

一つのラップする関数の定義 に対して、下記のプログラムのように、複数のデコレーター式を記述 することができます。このような場合は、下に記述されたデコレーター式から順番 に処理が行われます。

@decoratorA
@decoratorB
def func():
    pass

具体的には、上記のプログラムでは、下記の処理が行われます。

func = decoratorA(decoratorB(func))

詳細を説明すると長くなるので今回の記事ではでは具体的な説明を省略します。

デコレーター式の種類

デコレーター式にはいくつかの種類がある のでそれらについて簡単に説明します。

関数呼び出しを記述するデコレーター式

先程紹介した functools モジュールの wraps をデコレーター式に記述する際に、@wraps(func) のように、関数呼び出しをデコレーター式に記述 しました。

このような記述を行うことができるようにするためには、デコレーターの関数の定義 を今回の記事で説明した方法とは 若干異なる方法で行う 必要があります。ただし、その方法を説明すると記事がかなり長くなってしまうので、今回の記事では説明しません。また、wraps のようなデコレーターを 利用するだけ であれば、どのように定義するかを知る必要はありません。そのようなデコレータの定義の方法については、必要が生じた場合に説明したいと思います。

なお、そのようにデコレータ式を記述した場合でも、行われる処理が特に変わるわけではありません。例えば、下記の 1 ~ 3 行目のプログラムは、5 ~ 7 行目のプログラムと同じ処理を行います。

1  @decorator(arg)
2  def func():
3      pass
4
5  def func():
6      pass
7  func = decorator(arg)(func)
先程との違い
-@decorator
+@decorator(arg)
def func():
    pass

def func():
    pass
-func = decorator(func)
+func = decorator(arg)(func)

先程は 1 行目が @decorator、7 行目が decorator(func) でしたが、上記では 1 行目が @decorator(arg)、7 行目が decorator(arg)(func) のように変わっており、具体的には decoratordecorator(arg)置き換えているだけ です。従って、デコレータ式が行う 処理の性質は全く同じ です。

メソッドに対するデコレーター式

これまでは関数の定義に対してデコレーター式を記述しましたが、メソッドに対しても同様の方法 でデコレーター式を記述できます。

下記は A というクラスの add_1 というメソッドに対して @create_show_time のデコレータ式を記述 したプログラムです。

  • 2、3 行目:インスタンスの作成時に、属性 x に 1 を代入する
  • 6 ~ 8 行目:属性 x の値を 1 加算し、その値を表示する
1  class A:
2      def __init__(self):
3          self.x = 1
4        
5      @create_show_time
6      def add_1(self):
7          self.x += 1
8          print(self.x)
行番号のないプログラム
class A:
    def __init__(self):
        self.x = 1
        
    @create_show_time
    def add_1(self):
        self.x += 1
        print(self.x)

上記のプログラムを実行後に、下記のプログラムを実行して add_1 を呼び出すと、実行結果に処理時間が表示されることから、add_1処理時間を表示 する機能が追加された ラッパーメソッドになっている ことが確認できます。

a = A()
a.add_1()

実行結果

2
0:00:00

クラスに対するデコレーター式

クラスに対してもデコレーター式を利用して、クラスの機能を拡張したラッパークラスを定義する事ができます。ただし、下記の公式ドキュメントに「同じ概念がクラスにも存在しますが、あまり使われません」と記述されているように、クラスに対するデコレーター式はあまり使われないようなので、本記事では説明を省略します。

クラスに対するデコレーター式の処理については、下記のリンク先を参照して下さい。

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

これまでの記事では、デコレーターを関数として定義しましたが、デコレーターをクラスとして定義する事もできます。その説明は次回の記事で行います。

今回の記事のまとめ

今回の記事では、デコレーターとデコレーター式を利用したラッパー関数の定義について説明しました。

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

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル

次回の記事

  1. 今回の記事で紹介する @wraps(func) のように、デコレーター式に関数呼び出しを記述する場合は、1 つ以上の仮引数を持つ関数を定義する必要があります。その定義の方法については説明が長くなるので今回の記事では省略します 2

  2. 厳密には、関数のように呼び出す ことができる 呼び出し可能オブジェクト(callable object)を返せば良いことになっています。呼び出し可能オブジェクトについては、次回の記事で説明する予定です 2 3

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?