はじめに
今回は、「たまに見かけるけどよくわからない」
Pythonの便利なデコレータ機能について備忘録として書いていこうと思います。
Pythonデコレータとは何か
そもそもデコレータという考え方は、Pythonのデコレータ機能特有のものではなく、一般的にソフトウェア・デザインパターンに使われる考え方です。
ただPythonにおけるデコレータ機能はこのオブジェクト指向に基づいたいわゆるデコレータパターンのような考え方ではないです。
大元の考え方は一般的なプログラミングのデザインパターンに近いものがありそうですが、Pythonの場は関数に装飾を施すシンタックスシュガーになります。
シンタックスシュガー(糖衣構文)とはプログラミング言語において、任意の書き方に対して別の書き方ができるようにした構文です。
【参考】一般的なデコレータパターンとは
オブジェクト指向のデコレータパターンの定義は古く、ソフトウェアの再利用性の高いデザインパターンを考案したGoF(Gang of Four)により1990年代に登場してきました。
GoFによるデザインパターンの中でDecoraterパターンのみご紹介します。
デザインパターンはプログラミングによる開発者による生産性の差を埋めるための考え方で、エンジニアとしてぜひ全て習得したいものです。
Wiki上では下記のように定義されています。
Decorator パターン(デコレータ・パターン)とは、GoF(Gang of Four; 4人のギャングたち)によって定義されたデザインパターンの1つである。 このパターンは、既存のオブジェクトに新しい機能や振る舞いを動的に追加することを可能にする。
Decorator パターンの方針は、既存のオブジェクトを新しい
Decorator
オブジェクトでラップすることである。引用: wikipedia
引用元でも説明されていますが、デコレータパターンは対象クラスに「装飾をする(Decarateする)」意味合いを持ち、その本質は、クラスの中身を変更することなくクラスコードに機能を追加する考え方になります。
Python関数デコレータの使い方
それではデザインパターンの話で少し話がそれましたが、一般的なデコレータの考え方がわかったところで、Pythonにおけるデコレータ機能についてご紹介していきます。
Pythonのデコレータは下記のように定義されています。
(デコレータ) 別の関数を返す関数で、通常、
@wrapper
構文で関数変換として適用されます。引用: Python公式ドキュメント
基本的な関数デコレータの使い方
例えば下記のような関数があったとします。
# デコレータターゲット関数定義
def target_func():
print('デコレータの装飾ターゲットの関数だよ')
「デコレータの装飾ターゲットだよ」という文言を標準出力する、かなりシンプルな関数です。
この関数の前と後にデコレータ機能を使って装飾をしていきます。
まずは肝となるデコレータ関数の定義。
# 関数デコレータ定義
def decorator(func):
def wrapper(*args, **kwargs):
print('###ターゲットの前だよ###')
func(*args, **kwargs)
print('###ターゲットの後だよ###')
return wrapper
# デコレータターゲット関数定義
@decorator
def target_func():
print('デコレータの装飾ターゲットの関数だよ')
はい、これだけです。この状態でtarget_func()を呼び出すと下記のような実行結果が得られます。
###ターゲットの前だよ###
デコレータの装飾ターゲットの関数だよ
###ターゲットの後だよ###
「デコレータの装飾ターゲットの関数だよ」の前後に「ターゲットの前だよ」「ターゲットの後だよ」の文言が表示され、関数の出力前後が装飾されました。
これはログ出力などで使えそうですね。
少しコードについて解説します。
@decoratorが装飾ターゲットの関数(target_func)の最初に記述されます。
これは装飾ターゲットの関数が呼ばれると、呼ばれた関数が実行されるわけではなく@decorator、最初に定義した関数デコレータのdecorator関数が呼ばれ実行されます。
呼ばれたdecorator関数の引数にはfuncつまり、装飾ターゲットの関数が定義されdecorator関数の中で実行されます。
今回はfunc引数の前後にprintで標準出力させる文言を記載していますが、後で紹介するように認証機能なども作り込めます。
・wrapper関数部分は必ずしもwrapperでなくてよく、new_funcなどの別名関数でもラップできます。
・functoolsライブラリのwrapsでもデコレータ定義ができる⇨こちらで紹介します。
Pythonの組み込み関数デコレータ
先ほどは独自でデコレータを作成する方法をご紹介しました。
実はPythonには既に組み込まれているデコレータがあります。
@classmethod
@staticmethod
@property
Python のクラス生成時に有効になってくるデコレータで、こちらの記事でご紹介します。
引数を受け取る関数デコレータ
実は関数デコレータでも引数を定義することができます。
さらに、その引数値を装飾ターゲットに渡すことができるのです。
ではさっそく見ていきましょう。
# デコレータ関数
def decorator(name):
def _decorator(func)
def wrapper(*args, **kwargs):
print(f'###{name}関数の前だよ###.format')
func(*args, **kwargs)
print(f'###{name}関数の後だよ###')
return wrapper
return _decorator
# デコレータターゲット関数
@decorator('target_func')
def target_func():
print('デコレータの装飾ターゲットの関数だよ')
実行結果
###target_func関数の前だよ###
デコレータの装飾ターゲットの関数だよ
###target_func関数の後だよ###
引数を定義するラッパー関数(decorator
)でもうひと階層包んであげることで関数デコレータにも引数を渡すことができます。
引数を含む関数デコレータを使うことで、HTMLサーバサイドレンダリングなどで使えそうですね。
関数デコレータの上手い使い所(Flask連携例)
FlaskでのAPIの実装
簡単なFlaskのAPIサーバを考えます。
from flask import Flask
app = Flask(__name__)
@app.route('/')
def test_api():
return '認証は必要ないよ'
@app.route('/auth')
def test_api():
return '認証済みだよ'
今回2つのAPIを用意しました。
1つ目はURI[/」、2つ目はURI「/auth」
※お気づきかもしれませんが、Flaskの@app.route()も引数を持つデコレータです。
Flaskでのデコレータを使った認証
上記作成したAPI関数が呼ばれた際の認証に関数デコレータを応用できます。
リクエストヘッダーやクッキーに持っている認証情報とデータベースの認証情報を突合するなどの処理を認証用の関数デコレータとして定義できます。
このような認証用デコレータを作り、他のAPI関数でも同様に使い回すことで再利用性が高まります。
先ほどのFlask APIに対して認証用デコレータを使って装飾していきます。
今回の認証はHTTPリクエストヘッダーのAuthentication要素に特定の固定文字列(hogehoge)が入っており、その文字列を鍵として認証します。
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def test_api():
return '認証は必要ないよ'
@app.route('/auth')
@auth_required
def test_api():
return '認証済みだよ'
def auth_required(func):
def wrapper(*args, **kwargs):
# func実行の前に処理する。
if not request.headers.get("Authentication") == 'hogehoge':
return '認証キーが違うよ'
else:
func(*args, **kwargs)
return wrapper
HTTPヘッダーのAuthentication要素にhogehogeを入れて、/authにアクセスすると「認証済みだよ」」文言が返って来ます。
Authenticationが空、もしくは文字列が異なっていると「認証キーが違うよ」文言が返ってきて、APIが実行できません。
以上で、デコレータによる簡易的な認証を含んだAPI構築はおわりです。
簡単でしたね。
実際の開発ではDBが絡んできたり、ログインやセッションが絡んできたりともう少し複雑になります。
しかし、全体の考え方は同じなのでぜひ取り入れてみてください。
API認証はデコレータ内の装飾ターゲットの実行前に組み込む
おわりに
今回はPythonの関数デコレータについてご紹介しました。
基本的な書き方を中心にご紹介しましたがメタデータが書き変わるなどの課題もあります。
functoolsなどを用いることで解決できるため、改めて記事にしようと思います。
ご意見などあればこちらからお問合せください。
それでは素敵なPythonライフを!