Pythonのデコレータの最もシンプルな例(Progate修了レベル)

ProgateでPythonのレッスンを修了したばかりのあなたへ。「Pythonマスターしたぜ!」の気分になってDjangoやらFlaskやら実践編をぐぐってみたところわけのわからない「@(アットマーク)」記法にめまいがしている頃かと存じます。

心配無用、私のことです。この記事で10分で@をマスターしましょう。

@記法の正体

デコレータというらしいです。

Pythonはオブジェクト指向の言語(RubyやSwiftなどもそう)なので、関数もオブジェクトです。

関数がオブジェクトであるとは

つまりこんなこともできるということです。

関数自体を変数に代入
def func(int):
    return int + 1

var = func

print(var(1))

実行結果

2
関数の引数に関数

関数の引数に別の関数を使うこともできます。

def func1(func,text):
    return func + text

def func2(text):
    return text

print( func1(func2('Hello'),' world') )

実行結果

Hello world

引数にとった関数の引数を使いたい

デコレータとは、「関数を引数にとって、新しい関数を返す」関数と考えてもらってOKです。
▷厳密にいうと関数ではなく「callable」というらしいですが、まあ関数と思っていていいでしょう。ちなみに関数はcallableの一種だそう。

さて、関数A(関数B( C )) という例を考えましょう。

関数Bを引数にとるからには、その引数 C 関数Aの中で使いたいですね。

しかしこんな書き方はシンタックスエラーになります。

def func1(func(arg),text):
#実行結果:SyntaxError: invalid syntax

困りましたね。これではfunc1(関数A)にarg(関数Bの引数 C )を渡せません。

これを解決するには、こんな書き方をしなければなりません。

def func1(func,text1):
    def innerfunc(arg):
        def func3(text3):
            return ' [' + text3 + '] ' 
        return func(arg) + text1 + func3(arg) + arg
    return innerfunc

def func2(text2):
    return text2

print( func1(func2,' world')('Hello') )

実行結果

Hello world [Hello] Hello

ちょっとごちゃついてしまいましたが、みてほしいのはfunc2の引数である'Hello'func1の中で自由に使っている点です。

上のサンプルコードの解説

上でなにをやっているのかというと、

まずfunc1に引数をいれる

func1(func2, ' world')

この段階ではfunc2に引数は入れません。(引数をいれた関数=実行結果です。ここでは関数そのものを渡すので、引数は入れない。)
sum([1,2])は数値3であって関数sumではないですね。

すると、これはすなわち関数innerfuncを返します。

そうしたら、返された関数innerfuncに引数をわたせばよいのです。

func1(func2,' world')('Hello')
#func1(func2,' world') == innerfunc なので
#func1(func2,' world')('Hello') == innerfunc('Hello')

そうしたら、あとはfunc1の中のinnerfuncに引数'Hello'を入れた結果が出力されます。

さて、デコレータを使おう

デコレータは、上のコードがあんまりごちゃごちゃしているので簡単にまとめてしまおうというものです。

さっきのコードをデコレータを使って書くとこうなります。
※「wrapper」というのが増えてしまっていますが、これはあとで説明します。

def func1(text1):
    def wrapper(func):
        def innerfunc(arg):
            def func3(text3):
               return ' [' + text3 + '] ' 
            return func(arg) + text1 + func3(arg) + arg
        return innerfunc
    return wrapper


@func1(' world')
def func2(text2):
    return text2

print( func2('Hello') )

実行結果

Hello world [Hello] Hello

つまりfunc2 == デコレータ無しの場合の func1(func2,' world')ということです。

一般化して書くとこうなります。

@func1(引数A)
def func2(引数):

#と書いたとき、

func2(引数B)
#はデコレータがなかった時の
func1(func2(引数B),引数A) 
#を表す

func1の引数(wrapperを使う)

func1関数A)にfunc2関数B)以外の引数を入れる場合(上のケースがそうでしたね)は、func1(func,arg):のような形のままデコレータを使うとシンタックスエラーになってしまいます。

そういうときはwrapperを使います(別に名前はなんでもいいです)。

書き方は上のケースを参考にしてください。func1自体の引数には関数は入れません。関数を引数にとる関数を内部で新たに定義し、ワンクッション置く形になります。

wrapperが要らない場合

func1関数A)の引数にfunc2関数B)しか入れない場合はこの書き方は不要です。その場合はこんな風に書けばOKです。

def func1(func):
    def innerfunc(arg):
        def func3(text3):
           return ' [' + text3 + '] ' 
        return func(arg) + ' world' + func3(arg) + arg
    return innerfunc


@func1
def func2(text2):
    return text2

print( func2('Hello') )

実行結果

Hello world [Hello] Hello

デコレータを使わずに書くとこうなります。

def func1(func):
    def innerfunc(arg):
        def func3(text3):
           return ' [' + text3 + '] ' 
        return func(arg) + ' world' + func3(arg) + arg
    return innerfunc

def func2(text2):
    return text2

print( func1(func2)('Hello') )

まとめ

デコレータがわからないと、フレームワークはすべて呪文になってしまいます。そもそもそういうブラックボックスが嫌だからRuby on Railsを避けてPythonを選んでるんでしょ?(僕のことです)

ちなみにデコレータは複数つけられます。(その場合、実行は下からなので注意)

@func2
@func1
def func3(text):
    return text

ご質問などあれば気軽にコメントにどうぞ!

余談:Flaskでよくみる@app.route('/')

Flaskのルーティングで@app.route('/')みたいのがよくでてきますね。意味を知らなくても使えるしすごい楽なんだけど、これもデコレータですね。

そもそもappについては上の方でこういうコードで作ってると思います。

from flask import Flask

app = Flask(__name__)

または(この違いがわからない人はProgateに戻りましょう)

import flask

app = flask.Flask(__name__)

__name__はProgateでも習った__init__と形が似てますね。こういう人間が作りそうにない形のものはだいたいシステムで決まったものですよね。

__name__にはモジュールの名前が自動的に代入されています。Flask(__name__)つまり引数に__name__をとるFlaskクラスのインスタンスがappです。

Flaskクラスにはメソッドroute(self, rule, **options)が定義されているので、このruleに引数としてパス(URL)を渡すわけですね。

このへんの中身を全部把握しようとすると骨が折れるので不要と思います。ここはこんな理解でOKです↓。

@app.route('パス')
def パスごとの処理

__name__について詳しくはこちらがわかりやすかったです。
azuuun-memorandum.hatenablog.com