DjangoBrothers BLOG

2019/06/30

このエントリーをはてなブックマークに追加
Python

【Python】初心者向けにデコレータの解説

PythonやDjangoをやっていると@classmethod@login_requiredなど@マークがついたものを見ることがあるかと思います。

これはデコレータと呼ばれるもので、関数を定義するときにデコレータをつけることによって、その関数を機能的に装飾することができます。

@login_requiredの例

@login_required
def top(request):
    return render(request, 'top.html')

上のコードは、Djangoで@login_requiredを使用している例です。top関数の上につけることで、top関数を機能的に装飾しています。 具体的には、top関数を実行する前に「ユーザーがログイン状態かどうか」を判定する機能を付与しています。ユーザーがログイン状態の場合はtop関数を実行してtop.htmlを表示させ、非ログイン状態の場合は、top.htmlは表示せずにログインページを表示させます。

このようにデコレータを使えば、シンプルな記述で機能を付与することができます。

デコレータはPythonの入門書などでは解説されることは少なく、なかなか理解に苦しむものです。そのため、最初は「便利なおまじない」として覚えておくだけでも良いかと思います。

しかし、デコレータがどのように作られているかを理解してしまえば、自分でもオリジナルのデコレータを作れるようになって便利ですし、@login_required等が裏で何をしているかがわかるようになるので、コードに対する理解を深めることもできます。

この記事では、デコレータがどういう仕組みなのかをなんとなく理解できる状態になるように説明していきます。

前提知識

デコレータを理解する上では、前提となるPython知識が大事になります。これらを押さえておけば、デコレータも理解しやすくなります。

1つ1つ理解していきましょう。

1. 「関数そのもの」と「関数の実行」の違い

以下のコードでは、a関数を定義しています。そして、 aa()をprintしています。

関数そのものと関数の実行

def  a():
    return "hello"

print(a)
# <function a at 0x10c9eb1e0>  # 「a関数オブジェクト」が出力される

print(a())
# hello  # 「a関数の実行結果」が出力される

出力結果を見るとわかるように、aa関数そのものa()a関数の実行を表します。括弧をつけるかどうかの違いだけですが、得られる結果は異なるので明確に分けて考えなければいけません。 この記事では、前者を「関数オブジェクト」、後者を「関数の実行」という言葉を使って説明していきます。

2. 関数内関数

Pythonでは、関数内で関数を定義することができます。

関数内関数

def a():
    def b():
        return 'bです。'
    return b

print(a())  # b関数オブジェクトを出力
# <function a.<locals>.b at 0x107de8158>

a関数の中では、b関数を定義しています。そして、a関数は最後に「b関数オブジェクト」をreturnします。つまり、「a関数を実行するとb関数オブジェクト」が返ります。 式で表せば、a()=bです。

a関数の実行は「b関数オブジェクト」であり、「b関数の実行」ではありません。よって、bです。は出力されません。

a()=bということは、a()()=b()となるのでa()()で「b関数を実行」することができ、bです。を出力することができます。

関数内関数

def a():
    def b():
        return 'bです。'
    return b

print(a()())  # b関数の実行結果を出力
# bです。

3. 関数を引数にとる関数

Pythonは関数の引数として、関数をとることができます。

まずは、よく見る文字列を引数とする関数から。

文字列を引数にとる関数

def a(name):  # 文字列を引数に取る関数
    return 'hello! ' + name

print(a('jobs'))  # a関数の実行結果を出力
# hello! jobs

次に、関数を引数にとる例です。

関数を引数にとる関数

def a(func):  # 関数を引数に取る関数
    return func

def b(name):
    return 'hello! ' + name

print(a(b))  # a関数の実行結果を出力
# <function b at 0x103a05488>  # b関数が出力される

a関数は関数を引数にとり、その関数をreturnします。上の例だと、「a関数の実行=b関数オブジェクト」です。

a(b)=bということは、a(b)()でb関数を実行できます。正確には、b関数はname引数をとるので、a(b)('jobs')とします。

a関数の引数にb関数を渡して、b関数を実行する

def  a(func):
    return func

def  b(name):
    return 'hello! ' + name

print(a(b)('jobs'))
# hello! jobs

デコレータについて

ここまでを踏まえて、デコレータを使ってみます。

デコレータとは、対象の関数に対して、別処理を付け加える関数のこと。言葉だけだとよくわからないので実際にやってみます。

デコレータの例

# デコレータとなる関数
def a(func):
    def b():
        print('スタート')
        func()  # func()=c関数の実行 → 'c関数を実行しています。'
        print('おわり')
    return b

# デコレートされる関数
def c():
    print('c関数を実行しています。')

a(c)()  # b関数の実行(func=c)
# スタート
# c関数を実行しています。
# おわり

a関数を実行するとb関数オブジェクトが返ってきます。(a(c)=b) つまり、最後の行のa(c)()b()を意味しb関数を実行しています。

また、a(c)()ではa関数の引数funcにc関数オブジェクトを渡しています。つまり、b関数で使われているfunc()は「c関数の実行」を意味します。

b関数が実行されると、以下の流れで処理されます。

  1. 'スタート'がprintされる
  2. func(c関数)が実行される
  3. 'おわり'がprintされる

結果として、c関数の処理の前後に文字列を出力することができます。(c関数を装飾したことになります。)

以上の処理を@マークを使ってよりシンプルな書き方をできるようにしたものがデコレータと呼ばれるものです。書き方が違うだけで、実行される処理自体は全く同じです。

@マークを使うと以下のように書くことができます。

@マークを使ったデコレータの例

# デコレータとなる関数
def  a(func):
    def  b():
        print('スタート')
        func()  # func()=c関数の実行 → 'c関数を実行しています。'
        print('おわり')
    return b

# デコレートされる関数
@a
def c():
    print('c関数を実行しています。')

c()
# スタート
# c関数を実行しています。
# おわり

c関数の上に@マークをつけています。こうすることでc関数を実行すると、自動的にa(c)()が実行されることになります。つまり、b関数が実行されます。

@マークのついた関数を実行すると、デコレータ関数(デコレートされる関数)()のように、対象となる関数をデコレータ関数の引数に渡した上で、デコレータ関数を実行してくれるのです。

言い換えれば、デコレータ関数(a関数)は必ず、実行可能な関数をreturnする必要があります。

実用的な例

処理の前後に文字列を出力させるだけでは、デコレータのありがたみがイマイチわかりません。 少し実用的な例を考えてみます。

例えば、「お酒を飲む処理」を実行するdrink関数があったとします。drink関数は、名前、年齢と共に「ゴクゴク」と出力します。

まずは、デコレータを使わない例です。単純にdrink関数を定義して、それを呼び出しているだけです。

drink関数の実行(成人判定なし)

def  drink(name,  age):
    """飲酒する"""
    print(f'{name}({age})「ゴクゴク、ぷはーっ!」')

# drink関数を実行する(ageの値に関わらず実行される)
drink('Jobs',  21)
# Jobs(21)「ゴクゴク、ぷはーっ!」

drink('Rola',  10)
# Rola(10)「ゴクゴク、ぷはーっ!」

当然、このコードでは年齢に関わらず、「飲酒処理」が実行されます。

次に、年齢に応じて処理を切り替えたい場合を考えます。ageが20以上ならdrink関数を実行し、20未満ならdrink関数は実行しないようにします。デコレータをつかって、drink関数を実行する前に年齢を判定する処理を付け加えます。

drink関数の実行(デコレータで成人判定をする)

def adult(func):
    def checker(name, age):
        if age >= 20:
            print(f'{name}は大人なので処理を実行します。')
            func(name, age)  # func(≒drink関数)を実行
            print('処理が完了しました。')
        else:
            print(f'{name}は未成年なので処理は実行できません。')
    return checker

@adult
def drink(name, age):
    """飲酒する"""
    print(f'{name}({age})「ゴクゴク、ぷはーっ!」')

# drink関数を実行する。(ageの値によっては実行されない)
drink('Jobs', 21)
# Jobsは大人なので処理を実行します。
# Jobs(21)「ゴクゴク、ぷはーっ!」
# 処理が完了しました。

drink('Rola', 12)
# Rolaは未成年なので処理は実行できません。

drink関数の上にadultデコレータをつけているのでdrink('Jobs', 21)adult(drink)('Jobs', 21)を意味します。

当然、デコレータを使わずに、drink関数内でageを判定する処理を書いても良いのですが、デコレータを作っておくことで、@adultと書くだけで成人判定をできるようになります。 仮に、このあとsmoke関数(drink関数と似た喫煙処理をする関数)を作った場合、同様に@adultをつけるだけで、成人限定で実行可能な関数に設定することができます。