Python

Pythonのデコレータについて

今回は、Pythonデコレータについて話そうと思います。


はじめに。

エキスパートPythonプログラミング1に沿って勉強していた際に、Pythonのデコレーターという概念が出てきました。ちょっと本に書いてある内容では何を言っているかわからなかったので、いろいろ調べてみました。


デコレーターとは

デコレートとは修飾する。って意味になりますが、デコレータとは、簡単に言うと、ある関数を修飾するための関数とその仕組みです。例えば、ある関数があったとします。ここでは次のようなtest()関数とその実行スクリプトを見てみましょう。

サンプルスクリプト1


test.py

def test():

print('Hello Decorator')

test()


実行結果は次のようになります.

Hello Decorator

まあ当たり前ですよね。

このtest関数をデコレートしていきます。


デコレーターの簡単な例

上記のスクリプトをデコレーターを使ってデコレートしたものが次のようになります。


sample_deco1.py

def deco(func):

def wrapper(*args, **kwargs):
print('--start--')
func(*args, **kwargs)
print('--end--')
return wrapper

@deco
def test():
print('Hello Decorator')

test()


実行結果が次のようになります。

--start--

Hello Decorator
--end--

さて、これがどうなっているかを説明します。


なかみの簡単な説明

1行目def deco(func):から6行目return wrapperがデコレートするためのデコレーターの定義関数です。この関数に従ってデコレートします。8行目@decoは、「deco」というデコレーターの定義関数で次の関数をデコレートしますよ。ってことになります。

では、デコレーターの定義関数の中身について話していきます。

重要なのは3行目print('--start--')から5行目print('--end--')です。

1行目def deco(func):はデコレーター関数の宣言です。引数にfuncと指定していますが、ここには、デコレートされる関数であるtest()自体が引数として渡されています。

2行目def wrapper(*args, **kwargs):は、実際のデコレーターの中身を書くための関数です。よくわからなかったらこういうもんだと把握しながらとりあえず進みましょう。後で詳しく説明します。

さて、重要な3行目から5行目です。

4行目func(*args,**kwargs)では、引数として渡された関数が実行されます。

3行目print('--start--')と6行目print('--end--')では、5行目func(*args,**kwargs)の前と、後に実行される処理を示します。

で、*args, **kwargsは、デコレートする関数の引数です。まあどんな関数でも何も考えずにこういう風に書いておいたら大丈夫です(多分)


つまりデコレータで何ができるのか

デコレーターを利用することで、既存関数の処理の前後に自分自身で、処理を付け加えることができます。どういう状況でそういうことをするかということを考えてみました。僕自身もまだ勉強したてでわかっていない部分が多いのですが、デコレーターを使うことで、ライブラリとして提供されている関数を呼んだときに自動的に実行される処理を付加できるということになります。

Pythonでは、Javaなどで利用されるオーバーライドのような形で関数自体を書き換えてしまうことも可能ではあります。しかしながら、既存の処理には問題がないんだけど、それだけじゃなくて、違う処理も同時にさせたい。みたいな状況のときに、デコレータを使うんだと思います。

調べていたら、デコレーターを利用して、ある関数のベンチマークを行うプログラムが紹介されていました。

なるほど、ベンチマーク用のデコレーター関数を一つ作ってしまえば、あとはいろんな関数に@でデコレートしていくだけで、それぞれのベンチマークを計測できたりしますよね。便利ですね。


返り値を持つメソッドでのデコレーターの例

上記の例では、デコレートされるメソッドは、返り値を持ちませんでした。しかし、メソッドは当然ながら、返り値を持つ時もあります。ここでは、返り値を持つメソッドでのデコレータの例を記載します。


sample_deco2.py

def deco2(func):

import os
def wrapper(*args,**kwargs):
res = '--start--' + os.linesep
res += func(*args,**kwargs) + '!' + os.linesep
res += '--end--'
return res
return wrapper

@deco2
def test2():
return('Hello Decorator')

print(test2())


このスクリプトの実行結果が以下になります。

--start--

Hello Decorator!
--end--

ああ!?実行結果、変わってねえじゃねえか!!って?いや、微妙に変わってます。出力の一部がHello DecoratorからHello Decorator!に変わりました。プログラムの中身も結構変わっています。このプログラムをさっきのプログラムと比較しながら説明して行きます。

まず、さきほどのプログラムと比較して、デコレートされるメソッドdef test2():は、文字列の返り値return 'Hello Decorator'を持つようになりました。そして、print(test2())が、戻ってきた文字列を出力しています。

さっきまでは、デコレートされるメソッドは、文字列を出力するメソッドでしたが、今回デコレートされるメソッドは、文字列を生成するメソッドであるということです。何かを生成するメソッドをデコレートする場合、生成された結果が返ってくるようにデコレータを書かなければなりません。

デコレータの中身で先ほどと変わった場所をピックアップします。

5行目のres = func(*args,**kwargs) + '!'

7行目のreturn res

8行目のreturn wrapper

5行目では、func(**kwargs)を実行して返ってきた文字列の末尾に'!'を付け足してresに格納しています。

7行目ではresを返します。それを8行目のreturn wrapperでさらにデコレートされるメソッドに返しています。


つまりどういうことか

デコレータは、値を返すメソッドをデコレート可能なだけではなく、デコレートするメソッドの返り値を加工することが可能であるということです。これは非常に面白いことです。デコレータするのプログラムの出力結果にある特定の処理を加えることが可能と考えれば・・・?いろいろ楽しくなってきますね。

さらに言えば、例えば、htmlやxmlのようにタグで囲まれる記法はデコレータを利用すれば中身だけ生成するメソッドで記載できることになります。

あれ?例えば、htmlをデコレータで書くなら、ネストさせたいな・・・。はい。デコレータはネストすることもできます。


デコレータをネストさせる

複数のデコレータを組み合わせることも可能です。

例えば、以下のような状態です。


deco_sample3.py

def deco_html(func):

def wrapper(*args, **kwargs):
res = '<html>'
res = res + func(*args, **kwargs)
res = res + '</html>'
return res
return wrapper

def deco_body(func):
def wrapper(*args, **kwargs):
res = '<body>'
res = res + func(*args, **kwargs)
res = res + '</body>'
return res
return wrapper

@deco_html
@deco_body
def test():
return 'Hello Decorator'

print(test())


実行結果は以下のようになります。

<html><body>Hello Decorator!</body></html>

このプログラムは、これまでのプログラムがわかれば、何も難しくありません。def test(str):をデコレートするデコレータが二つになっただけです。下にあるものから順番に処理されて行きます。

なかなか楽しくなってきましたね。デコレータって色んなことができそうだ。あれ?そういえば、引数をもつメソッドにデコレータを使うことはできないんだろうか・・・。できます。


引数をもつメソッドをデコレートする

引数を持つメソッドのデコレートは以下のように作成します。


deco_sample4.py

def deco_p(func):

def wrapper(*args, **kwargs):
res = '<p>'
res = res + func(args[0], **kwargs)
res = res + '</p>'
return res
return wrapper

@deco_p
def test(str):
return str

print(test('Hello Decorator!'))


実行結果は以下のようになります。

<p>Hello Decorator!</p>

さて、先ほどまでのプログラムと変わった部分はどこでしょう。

func(args[0], **kwargs)です。実はデコレータはデコレートするメソッドが引数を持っているかどうかに関係なく、引数を受け取れる状態になっています。引数を利用する場合は、argsから引数を取り出して、funcに渡して上げます。そして、面白いのは、argsがタプルであるという点です。

タプルということはどういうことでしょうか。とりあえず少なくともビルトインメソッドのlen()は利用できますね。ということは、引数の数によって、デコレータの挙動を変えたりできるということです。ていうか、そもそもデコレータに引数って渡せないのかな・・・。渡せます。


デコレータに引数を渡す


deco_sample5.py

def deco_tag(tag):

def _deco_tag(func):
def wrapper(*args, **kwargs):
res = '<'+tag+'>'
res = res + func(*args, **kwargs)
res = res + '</'+tag+'>'
return res
return wrapper
return _deco_tag

@deco_tag('html')
@deco_tag('body')
def test():
return 'Hello Decorator!'

print(test())


出力結果は以下のようになります。

<html><body>Hello Decorator!</body></html>

疲れてきた説明を省きますが、ここではデコレータをネストしています。


おわり

今回は、デコレータについて色々試して、その内容をここに書いてみました。デコレータって面白いですね。いろんなことができそうです。例えば、これをジェネレータと組み合わせてみたら・・・、本当に色んなことができそうです。一番最初に書いた参考URLにはもっと詳しいデコレータの説明がありますので、興味がわけばどうぞ。

ではでは。


追記 2018.7.6

この記事を書いたのは、2015年1月でした。

もう公開から3年半経っているのですが、今でも記事を「いいね」してくれる人がいてありがたいです。

3年半経って、僕もPythonのデコレータについて理解が深まりました。

今だからできる話を追記しておきます。


別の例

当時は思い浮かばなかったのですが、今ならデコレーターでこういう使い方もできるという使い方を紹介します。(実際にやるかどうかはおいといて)

例えば、以下のモジュールがあったとします。


my_method.py

def my_method(*args, **kwargs):

if 'test_key' in kwargs:
print('test_keyの値は、[{}]'.format(kwargs['test_key']))
else:
print('test_keyに値は、入っていません。')

my_method()
my_method(test_key="テスト用の値")


モジュールの実行結果は以下です。

test_keyに値は、入っていません。

test_keyの値は、[テスト用の値]

デコレーターを使うとまとめて挙動を変えることができます。


my_method_2.py

def my_decorator(target_function):

def wrapper_function(*args, **kwargs):
kwargs['test_key'] = 'デコレータで書き換えられた値'
return target_function(*args, **kwargs)
return wrapper_function

@my_decorator
def my_method(*args, **kwargs):
if 'test_key' in kwargs:
print('test_keyの値は、[{}]'.format(kwargs['test_key']))
else:
print('test_keyに値は、入っていません。')

my_method()
my_method(test_key="テスト用の値")


実行結果は以下になります。

test_keyの値は、[デコレータで書き換えられた値]

test_keyの値は、[デコレータで書き換えられた値]

このように一括して、引数を変更することができます。なんに使うの?というのに答えるいい感じの例がすぐにでて来ないのですが、こういうこともできるというのを知っておくのは良いかと思います。


デコレーターはシンタックスシュガー

デコレーターは、シンタックスシュガーです。

シンタックスシュガーというのは、ある用途のプログラムを書きやすくするために作られた文法です。

Pythonに限らず、他の言語でもシンタックスシュガーの文法があります。

シンタックスシュガーである、ということは、別の書き方もあるということです。

例えば、以下のモジュールがあったとします。

def twice(func):

def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper

@twice
def add(x, y):
return x + y

print(add(1, 3))

これの実行結果と、次の実行結果は同じです。

def twice(func):

def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper

def add(x, y):
return x + y

print(twice(add)(1, 3))

twiceという関数にadd関数を渡して出てきた関数に 1, 3を渡して実行した結果をprintしてます。

こんな風に書くと何がやりたいのかわかりにくいですよね。というわけでデコレータができた、ということだと思います。


参考URL





  1. 2018/2/26にエキスパートPythonプログラミング改訂2版が発売されましたね。今から勉強する人は2版がおすすめです。