Pythonのデコレーター関数について、記載します。

Pythonのデコレーター関数を利用すれば、関数の前後に特定の処理を実行できます。いわゆるアスペクト指向的なことが可能です。

def greeting():
  print 'hello'

hello()

上記の実行結果は以下のとおりです。

Hello

関数greertingで出力される文字の前に「I said」、後に「to him」を常に出力したいと思います。

これを実現するのが以下のコードです。

def nice_greeting(func):
  def inner():
    print 'I said'
    func()
    print 'to him'
  return inner

def greeting():
  print 'Hello'

f = nice_greeting(greeting)
f()

上記の実行結果は、以下のとおりです。

I said Hello to him

この動きを説明します。

関数nice_greetingは、関数を引数に取る関数です。ここでは、先ほどの「hello」だけ出力する関数greetingを引数にとっています。
nice_greetingの中では、さらに関数innerを定義してます。Pythonは関数の中にさらに関数を定義できます。

関数innerの中では、nice_greetingの引数funcの実行前と実行後に’I said’、’to him’をそれそれ出力する機能を持っています。

最終的に、関数nice_greetingは、関数innerを戻り値として返します。

つまり、関数nice_greetingは引数で定義された関数funcの前に’I said’、後に’to him’を出力する関数を返す機能を持っています。

そしてメインの処理の部分

f = nice_greeting(greeting)
f()

では、greetingを引数にしたnice_greetingの関数を変数fに代入して、f()でその関数を実行しています。

今度は、Helloの部分をGood Byeに変更してみます。これを実現するのは以下のコードになります。

def nice_greeting(func):
  def inner():
    print 'I said'
    func()
    print 'to him'
  return inner

def greeting():
  print 'Hello'

def goodbye():
  print 'Good Bye'

f = nice_greeting(goodbye)
f()

先ほどのコードとの違いは以下のとおりです。

・関数goodbyの追加
・デコレーター関数nice_greetingの引数の変更(greeting→goodbye)

上記の関数の実行結果は以下のとおりです。

I said Good bye to him

つまり、関数を実行する際にいつも書く固定の処理を一箇所に集約できます。

より、実践的な利用方法は以下のとおりです。

def logging(func):
  def inner():
    logger.info(func.__name__ + " start")
    func()
    logger.info(func.__name__ + " end")
  return inner

def get_user():
  print "hoge@example.com"

f = logging(get_user)
f()

上記は、特定の関数が実行された場合に、実行前に「関数名 start」、実行後に「関数名 end」をログ出力します。実行結果は以下のとおりです。

get_user start
hoge@example.com
get_user end

このデコレーター関数を上手に利用すれば、コードをスリム化出来ます。

しかし、これは便利なのですが、毎回毎回以下のように書くのは面倒です。

f = logging(get_user)
f()

これは、以下のように書き換えることができます。

def logging(func):
  def inner():
    logger.info(func.__name__ + " start")
    func()
    logger.info(func.__name__ + " end")
  return inner

@logging
def get_user():
  print "hoge@example.com"

get_user()

変更点は以下のとおりです。

get_user関数の定義している部分の先頭に@loggingを追加
logging(get_user)としていたのを、普通にget_user()と呼び出すように変更
この書式であれば、ロギング処理を行いたい関数の先頭に@loggingと記載して、普通に関数を呼び出すだけで、ロギング処理が行われます。

ところで、今までの説明だと引数のない関数にしかロギング処理を定義できません。以下のコードを見て下さい。

def logging(func):
  def inner():
    logger.info(func.__name__ + " start")
    func()
    logger.info(func.__name__ + " end")
  return inner

@logging
def get_user():
  print "hoge@example.com"

@logging
def get_user_with_name(name):
  print "name +"":hoge@example.com"

get_user()

get_user_with_name('TaroYAMADA')

上記のコードを実行すると、次のエラーになります。

get_user start
hoge@example.com
get_user end
Traceback (most recent call last):
  File "func2.py", line 18, in <module>
    get_user_with_name('TaroYAMADA')
TypeError: inner() takes no arguments (1 given)

引数の数が合わないエラーです。関数loggingの中の関数innerで定義している関数は引数を持っていません。これでは、loggingのデコレータは引数を持たない関数しか適用出来ず、使い方がかなり限定的になってしまいます。

そこで、以下のように記述すれば問題は解決します。

def logging(func):
  def inner(*args,**kwargs):
    logger.info(func.__name__ + " start")
    func(*args,**kwargs)
    logger.info(func.__name__ + " end")
  return inner

@logging
def get_user():
  print "hoge@example.com"

@logging
def get_user_with_name(name):
  print(name + ':hoge@example.com')

get_user()

get_user_with_name(name = 'Taro YAMADA')

変更点は以下のとおりです。

関数loggingのinner関数の引数に*args,kwargsを追加
関数loggingのfunc関数の引数に*args,
kwargsを追加
*args,**kwargsは可変長引数と言われるものです。詳細は、こちらをご参照下さい。

可変長引数を使えば任意の数の引数を取ることが出来ます。

例えば、上記の例では、関数get_user_with_nameは以下のように動作します。

  1. get_user_with_name関数が呼ばれる。その際に引数としてname = ‘Taro YAMADA’が渡される。

  2. logging関数が呼ばれて、以下の処理が実行される。

    f = logging(get_user_with_name)
    f(name = 'Taro YAMADA')
    
  3. fの実態は、関数loggingの戻り値であるinner関数になります。なので、ここでは、innner関数の引数にname = ‘Taro YAMADA’が渡されることになります。

  4. 3で渡されたname = ‘Taro YAMADA’は、**kwargsの可変長引数の定義により、{‘name’ : ‘Taro YAMADA’}の形で関数に渡されます。

  5. 関数innerの中の関数funcに**kwargs(つまりname = ‘Taro YAMADA’)が渡されます。

といった形で、実行されます。