デコレータ

当サイトではアフィリエイトプログラムを利用して商品を紹介しています。

本記事では Python のデコレータについて説明します。以前の記事をあらためて読み直すと、すごく分かり難かったので、思い切ってリライト(書き直し)してみました。スモールステップで、なるべく丁寧に分かりやすく書くことを心がけたので、以前に読まれて「なんだかよくわからなかったよ」という方も、もう一度読んでいただけたら嬉しいです。

デコレータで関数に機能を追加する

簡単な例から始めます。最初に関数オブジェクト func を受け取って、内部で定義した関数を返す関数 deco を定義します(このような関数を 高階関数 といいます)。

# PYTHON_DECORATOR

# In[1]

# ["Hello"を表示する関数]を返す関数を定義
def deco(obj):
    def hello():
        print("Hello!")
    return hello

deco() は任意のオブジェクト obj を受け取るように設計されていますが、obj に対して何か処理するわけではありません。内部で定義した関数 hello() を返すだけです。hello() は “Hello!” を表示する関数です。ちなみに、deco は decorator の略で、装飾する側の関数として用意したので、こう名付けています。
 
deco() を実行して、変数 my_func に格納してみます。繰り返しますが、deco() には何を渡しても動作に何の影響も与えません。何でもいいのですが、とりあえず数値 0 を渡すことにします。

# In[2]

my_func = deco(0)

 my_func には deco() の戻り値 hello が入っています。試しに実行してみましょう。

# In[3]

my_func()
# Hello!

もちろん、実行結果は文字列 “Hello!” となっています。
 
さて、次に文字列 “Morning!” を表示する関数を定義します。

# In[4]

# "Morning!"を表示する関数を定義
def morning():
    print("Morning!")

まず、そのまま実行してみましょう。

# In[5]

morning()
# Morning!

今度は morning() を deco で装飾 (デコレート) して再定義してから実行してみます。関数をデコレータで装飾するときは @ 記号を使います。

# In[6]

# morning()をdecoで装飾して再定義
@deco
def morning():
    print("Morning!")

morning()
# Hello!

不思議なことに、morning() で定められた処理は完全に無視されて、deco() の内部で定義された hello() の実行結果が表示されました。関数オブジェクト morning の正体を調べてみましょう。

# In[7]

morning
# <function __main__.deco.<locals>.hello()>

morning は deco のローカル関数 hello への参照となっています。まさに、この「参照の差し替え」がデコレータの本質なのです。
 
ここまでの処理を @ を使わずに書き直すと、次のようになります。

# In[8]

def deco(obj):
    def hello():
        print("Hello!")
    return hello

def morning():
     print("Morning!")

deco(morning)()
# Hello!

@deco がなくなっている以外に、何も変わってないように思えますが、最後の行 deco(morning)() に注目してください。これは deco() に morning を渡して、戻り値の hello 関数を得て、() によって実行するという処理を表しています。
 
普段、こんな書き方をあまりしないし、意味が捉えにくいし、可読性の低いコードと言えるかもしれません。デコレータは、こうした記法を避けるためのシンタックス・シュガー (簡略記法) です。
 
しかし、これだけの説明では、デコレータ (装飾) のイメージが掴めません。では、morning に deco の内側で定義されている hello 関数の機能を付加するためには、どうしたらいいでしょうか?
 
答えは簡単。hello() に受け取った関数を実行する一文を加えておけばいいのです:

# In[9]

def deco(func):
    def hello():
        print("Hello!")
        func()  # ここで受け取った関数を実行
    return hello

# morningをdecoで装飾
@deco
def morning():
    print("Morning!")

morning を実行してみましょう。

# In[10]

morning()
# Hello!
# Morning!

 ”Hello!” と “Morning!” が両方表示されました。皆さんも色々なテストコードを書いて、デコレータの使い方に慣れてください。

コメント

  1. HNaito より:

    デコレータの仕組みの理解が進みました。これからはコードの中に@deco が出てきてもあせらず、deco関数の中で追加された機能を追っていけると思います。
    下記は誤植と思われますので、ご確認ください。
    In[4]の上の文章で、”Good morning!” → “Morning!”

    • あとりえこばと より:

      Python を学習していくうえで、デコレータに引っかかる人はかなり多いようです。構造をしっかり理解しておかないと、使いこなすのはなかなか難しいですよね。私自身も、以前は他の人が書いたコードにデコレータがあると読むのに苦労した経験があります。説明するのもなかなか大変で、試行錯誤しながら記事を書き直したりしました。
      「理解が進んだ」とおっしゃってくださって、とても嬉しいです。(^^)

  2. HNaito より:

    In[7] プログラムの実行結果は、下記のようになりました。
    <function __main__.deco..hello()>
    print(morning) の実行結果は、
    <function deco..hello at 0x7f137e2c5e50>
    となり、print文のほうがより正確な表現をしているように思えましたが、両者の違いはどのように解釈しておけばいいのでしょうか。

    • HNaito より:

      表示がおかしくなりましたね。
      deco. と .hello の間には、 が入っています。

      • HNaito より:

        前回記事の In[3] プログラムの実行結果でも同じように
        ”で囲まれた locals が消えていました。

      • あとりえこばと より:

        マークアップ言語の仕様上、”<” や “>” をそのままコメントフォームに送信すると表示がおかしくなるようです。ご面倒をおかけしますが、”<” は “&lt;”、”>” は “&gt;” を記述してください。m(_ _)m
        私が記事を書く際にも同じようにしなければなりませんが、しょっちゅう忘れて表示がおかしくなります。前回記事の In[3] の実行結果は修正しておきました。

        • HNaito より:

          【再投稿】
          In[7] プログラムの実行結果は、下記のようになりました。
          print(morning) の実行結果は、となり、print文のほうがより正確な表現をしているように思えましたが、両者の違いはどのように解釈しておけばいいのでしょうか。

          • HNaito より:

            【再々投稿】
            In[7] プログラムの実行結果は、下記のようになりました。
            <function __main__.deco.<locals>.hello()>
            print(morning) の実行結果は、
            <function deco.<locals>.hello at 0x7f137e2c5e50>
            となり、print文のほうがより正確な表現をしているように思えましたが、両者の違いはどのように解釈しておけばいいのでしょうか。

            • あとりえこばと より:

              関数オブジェクトをそのまま記述すると、オブジェクトの存在場所(空間)とオブジェクト名を示します。一方、print() に関数オブジェクトを渡すと、オブジェクトの場所(オブジェクトがグローバル空間にあるときは省略)に加えて、そのオブジェクトがメモリ上のどこに存在するかを示してくれます。たとえば、次のような関数を定義したとします。

              def hello():
                print(“Hello!”)

              このとき、hello()を実行すると、

              <function __main__.hello()>

              が表示されます。__main__ は、このオブジェクトがグローバル名前空間にあることを示します。一方で、print(hello)を実行すると、

              <function hello at 0x7f684ede40d0>

              のように表示されます。0x7f684ede40d0 はこのオブジェクトに割り当てられたメモリアドレスです。

              • HNaito より:

                hello( ) は 関数名、hello は関数のメモリ上のアドレスを指していると理解しました。そうすると、In[2] プログラムの下の文で、
                my_func には deco() の戻り値、つまり deco() の内側で定義されている hello() が入っています → my_func には deco() の戻り値、hello が入っています。
                としたほうが、In[3] プログラムの my_func( ) が素直に hello( ) と読めると思いました。