【Python】デコレータを直感的に理解する

アイキャッチ画像
  • デコレータの仕組みとは?
  • デコレータを別の書き方で記述するとどうなる?

本記事ではこのような疑問を解決します。


Pythonにおいてデコレータというと、関数定義の上に「@〇〇」と記述するあいつのことです。

見た目的に少しイレギュラーな感じがして、苦手意識を持っている方は多いのではないでしょうか。

用意されたデコレータを使う分にはそのまま記述すればいいですが、
デコレータを自作して必要な処理を記述したい時にはある程度の理解が必要です。


そこで今回はPythonにおけるデコレータを直感的に理解できるようにわかりやすく解説します。

本記事を読めばデコレータに対して感じていた苦手意識が吹き飛ぶことでしょう!

あわせて読みたい

エンジニアが副業を始めるには?エンジニアの副業にはどんな種類がある? 本記事ではこのような疑問を解決します。副業がブームになっている昨今、エンジニアほど副業をやりやすい職業はないでしょう。副業に関心があったり、副業をしてみたいと思っ[…]

アイキャッチ画像

そもそもデコレータは何のためにあるのか?

まずは結論から。

デコレータはすでにある関数の中身を変更することなく、
機能の追加や変更を効率的に行うため
にあります。


なぜデコレータが必要かというと、
既存の関数に対する機能の追加や変更を行うのに、
デコレートを使うことで簡単な記述で済ませられる
からです。


例えば、関数の実行時に「処理開始」、完了時に「処理終了」と出力させる処理を追加するとします。

この処理を追加したい関数が1つであればいいですが、
何十個もある場合、一つ一つの関数の中身を変更するのはとても面倒です。


しかし、最初に1つのデコレータ関数を定義しておけば、
あとは機能を追加したい関数の定義の上に「@○○」と記述するだけで済みます。



したがって、デコレータを使えば、関数の中身を変更せずにいくつもの関数に処理を追加できるのです。

デコレータの仕組み

デコレータの正体は「関数を引数として受け取り、戻り値として関数を返す関数」です。


そもそもPythonではすべてのものをオブジェクトとして扱います。

つまり、関数もオブジェクトです。

オブジェクトとして扱うということは変数に格納したり、
関数の引数や戻り値になったりできるということです。

また、関数は定義されたあとに、
「関数を呼び出す」「関数をオブジェクトとして扱う」かの2つの使われ方をします。


コードで見てみましょう。

def sample_function():
    print("テキスト")

# 関数を呼び出す(「テキスト」と出力される)
sample_function()

# 関数をオブジェクトとして扱う(ここでは変数に格納)
func_obj = sample_function
func_obj()  # ()を付けて実行する(「テキスト」と出力される)

そして、関数を受け渡しするということは関数をオブジェクトのまま扱うということです。

したがって、
「関数を引数として受け取る」とは「関数をオブジェクトのまま引数として渡す」ことであり、
「関数を戻り値として返す」とは「関数をオブジェクトのまま戻り値として返す」ことなのです。

あわせて読みたい

フリーランスエンジニアが案件獲得方法とは?自ら営業せずに案件を獲得するには?実務経験1年未満でも大丈夫なの? 本記事ではこのような疑問を解決します。これからフリーランスエンジニアとして独立したい方は、兎にも角にも案件の獲得が急務です[…]

アイキャッチ画像

デコレータを理解する近道はシンタックスシュガー(糖衣構文)

「デコレータはつまりこういうことをしてるんだよ」ということがわかれば理解も早まります。

ここで以下のコードを見てみましょう。

def sample_decorator(func):
    print("デコレータ登場!")
    return func

@sample_decorator
def sample_function():
    pass

上記のコードを実行するとどうなるでしょうか?

関数は定義されているだけで実行されているわけではないから・・・何も出力されない!

と、考えた方は多いのではないでしょうか。

実はそれは間違いで、以下のように出力されます。

デコレータ登場!

なぜ関数が実行されていないのに上記のような出力がされるのか?

この謎を紐解いてくれるのがシンタックスシュガー(直訳:糖衣構文)です。
※以後、糖衣構文とする


糖衣構文とはプログラミング言語において、ある構文を別の簡単な書き方で記述したもののことです。


上記のコードでいうと、
記述されているデコレータはこれから確認するコードの糖衣構文になっています。

つまり、先ほどのコードを別の書き方で記述すると以下のようになります。

def sample_function():
    pass

sample_function = sample_decorator(sample_function)

よって、デコレータ関数(sample_decorator())が実行されているため、
先ほど見たような出力結果になったのです。


そしてデコレータを扱う場面では、
定義する関数に引数や戻り値があったり、
デコレータに引数があったりと、いくつかのパターンがあります。


そこで本記事ではパターン別に、
①糖衣構文であるデコレータと、
②デコレータを別の書き方で記述したものを比べながら理解を深めていきましょう。

①引数なしの関数をデコレートする(基本形)

@デコレータ関数
def 関数():
  #関数の処理

# 引数なしの関数をデコレートする(基本形)
def decorator(func):
    def wrapper():
        print("始まり!")
        func()  # myfunc()が実行されるところ
        print("終わり!")
    return wrapper


# デコレータ使用
@decorator
def myfunc():
    print("メイン処理!")

myfunc()  # 実行結果を後述


# 糖衣構文
def myfunc():
    print("メイン処理!")

decorator(myfunc)()

myfunc()の実行結果:

始まり!
メイン処理!
終わり!

②戻り値ありの関数をデコレートする

@デコレータ関数
def 関数():
  #関数の処理
  return 戻り値

# 戻り値ありの関数をデコレートする
def decorator(func):
    def wrapper():
        print("始まり!")
        res = func()
        print(res)
        print("終わり!")
    return wrapper


# デコレータ使用
@decorator
def myfunc():
    return "メイン処理!"

myfunc()  # 実行結果を後述


# 糖衣構文
def myfunc():
    return "メイン処理!"

decorator(myfunc)()

myfunc()の実行結果:

始まり!
メイン処理!
終わり!

③引数ありの関数をデコレートする

@デコレータ関数
def 関数(引数):
  #関数の処理
  return 戻り値

# 引数ありの関数をデコレートする
def decorator(func):
    def wrapper(*args, **kwargs):
        print("始まり!")
        res = func(*args, **kwargs)
        print(res)
        print("終わり!")
    return wrapper


# デコレータ使用
@decorator
def myfunc(message):
    return message

myfunc("メイン処理!")  # 実行結果を後述


# 糖衣構文
def myfunc(message):
    return message

decorator(myfunc)("メイン処理!")

myfunc(“メイン処理!”)の実行結果:

始まり!
メイン処理!
終わり!

④複数のデコレータを付ける

@デコレータ関数①
@デコレータ関数②

def 関数(引数):
  #関数の処理
  return 戻り値

# 複数のデコレータを付ける
def decorator(func):
    def wrapper(*args, **kwargs):
        print("始まり!")
        res = func(*args, **kwargs)
        print(res)
        print("終わり!")
    return wrapper

def decorator2(func):
    def wrapper(*args, **kwargs):
        print("始まり!@2")
        res = func(*args, **kwargs)
        print(res)
        print("終わり!@2")
        return "「decorator2」の戻り値"
    return wrapper


# デコレータ使用
@decorator
@decorator2
def myfunc(message):
    return message

myfunc("メイン処理!")  # 実行結果を後述


# 糖衣構文
def myfunc(message):
    return message

decorator(decorator2(myfunc))("メイン処理!")

myfunc(“メイン処理!”)の実行結果:

始まり!
始まり!@2
メイン処理!
終わり!@2
「decorator2」の戻り値
終わり!

⑤引数ありのデコレータを付ける

@デコレータ関数(引数)
def 関数(引数):
  #関数の処理
  return 戻り値

# 引数ありのデコレータを付ける
def decorator(message2):
    def decorator_inner(func):
        def wrapper(*args, **kwargs):
            print("始まり!")
            res = func(*args, **kwargs)
            print(res, message2)
            print("終わり!")
        return wrapper
    return decorator_inner


# デコレータ使用
@decorator("がんばります!")
def myfunc(message):
    return message

myfunc("メイン処理!")  # 実行結果を後述


# 糖衣構文
def myfunc(message):
    return message

decorator("がんばります!")(myfunc)("メイン処理!")

myfunc(“メイン処理!”)の実行結果:

始まり!
メイン処理! がんばります!
終わり!

⑥複数の引数ありのデコレータを付ける

@デコレータ関数①(引数)
@デコレータ関数②(引数)

def 関数(引数):
  #関数の処理
  return 戻り値

# 複数の引数ありのデコレータを付ける
def decorator(message2):
    def decorator_inner(func):
        def wrapper(*args, **kwargs):
            print("始まり!")
            res = func(*args, **kwargs)
            print(res, message2)
            print("終わり!")
        return wrapper
    return decorator_inner

def decorator2(message3):
    def decorator_inner(func):
        def wrapper(*args, **kwargs):
            print("始まり!")
            res = func(*args, **kwargs)
            print(res, message3)
            print("終わり!")
            return res
        return wrapper
    return decorator_inner


# デコレータ使用
@decorator("がんばります!")
@decorator2("ヤッホー!")
def myfunc(message):
    return message

myfunc("メイン処理!")  # 実行結果を後述


# 糖衣構文
def myfunc(message):
    return message

decorator("がんばります!")(decorator2("ヤッホー!")(myfunc))("メイン処理!")

myfunc(“メイン処理!”)の実行結果:

始まり!
始まり!
メイン処理! ヤッホー!
終わり!
メイン処理! がんばります!
終わり!

@functools.wrapsを使う

デコレータを作成する時によく使われるのが@functools.wrapsです。

@functools.wrapsはデコレータを付けた関数のメタ情報が失われるのを防ぎます。

関数のメタ情報とは具体的には関数名やドキュメント文字列のことです。


先ほど説明した①引数なしの関数をデコレートする(基本形)のデコレータを例に確認しましょう。

@decoratorを付けたmyfunc()の__name__や__doc__を参照すると、
「myfunc」ではなく「wrapper」が返ってきます。

つまり、myfunc()が@decorator内のラッパー関数であるwrapper()に置き換えられてしまうのです。

def decorator(func):
    def wrapper():
        """
        こちらはwrapperです。
        """
        print("始まり!")
        func()
        print("終わり!")
    return wrapper

@decorator
def myfunc():
    """
    こちらはmyfuncです。
    """
    print("メイン処理!")

print("関数名:", myfunc.__name__)
print("ドキュメント文字列:", myfunc.__doc__)

実行結果:
関数名: wrapper
ドキュメント文字列: こちらはwrapperです。

よって、@decorator内のラッパー関数であるwrapper()に@functools.wrapsを付けます。

import functools


def decorator(func):
    @functools.wraps(func)  # ここに@functools.wrapsを付ける
    def wrapper():
        """
        こちらはwrapperです。
        """
        print("始まり!")
        func()
        print("終わり!")
    return wrapper

@decorator
def myfunc():
    """
    こちらはmyfuncです。
    """
    print("メイン処理!")

print("関数名:", myfunc.__name__)
print("ドキュメント文字列:", myfunc.__doc__)

上記のコードでmyfunc()の__name__や__doc__を参照すると、
しっかりと「myfunc」が返ってくることを確認できます。

実行結果:
関数名: myfunc
ドキュメント文字列: こちらはmyfuncです。

まとめ

コードのメンテナンス性と可読性を上げてくれるデコレータ。

理解の鍵はシンタックスシュガー(糖衣構文)です。

「デコレータはつまりこういうことをしている」ということがわかれば、
デコレータの自作も容易くできるのではないでしょうか。

本記事があなたのPython開発のお役に立てたら幸いです。