Pythonの関数の部分適用を行うfunctools.partial、デコレータを書くときなどに使うfunctools.wrapsについて。
えーと、社内の有志でbottle.pyのソースコードリーディングをやったんだよね。んで、まぁ、functoolsの理解が弱くて読むのに詰まったので、一旦自分の理解としてまとめておく。デコレータとか普段自分で実装する機会あまりないので、そのあたりの理解を深めることができたのも収穫だった。やっぱ勉強会は少人数の方がいいね。やる気ある人だけで催すクローズドなやつがいい。全員発表ルールの。会員制クラブみてーなやつ(違うか)。



0. functoolsについて

まず、functoolsとはなんなのか?という話なんだけど、公式を見てみると次のような記述がある。

9.8. functools — 高階関数と呼び出し可能オブジェクトの操作
バージョン 2.5 で追加.

functools モジュールは高階関数、つまり関数に対する関数、あるいは他の関数を返す関数、のためのものです。一般に、どんな呼び出し可能オブジェクトでもこのモジュールの目的には関数として扱えます。

うーん、日本語でおk。原文を読んでもそのまんま。とりあえずドキュメント読んでもニワトリ並の脳みそのおれにはわからなかった。そんなわけでfunctools.partialとfunctools.wrapsについていろいろ調査してみて理解した結果が以下である。

1. functools.partial

こいつは早い話、関数の部分適用を行うものである。冒頭でも述べた通り。以下がサンプルコード。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import functools

def calc(x, y, mode):
    if mode == '+':
        return x + y
    elif mode == '-':
        return x - y
    elif mode == '*':
        return x * y
    elif mode == '/':
        return x / y

def main():
    print '--------- call calc ---------'
    print 'calc(3, 1, "-") = %d' % calc(3, 1, '-')
    print '--------- make minus(x, y) using functools.partial ---------'
    minus = functools.partial(calc, mode='-')
    print 'minus(3, 1) = %d' % minus(3, 1)
    print 'minus(1, 1) = %d' % minus(1, 1)
    print 'minus(7, 9) = %d' % minus(7, 9)

if __name__ == '__main__':
    main()

calc(x, y, mode)は、第一引数、第二引数に数値を受け取り、第三引数に渡される四則演算の演算子に従って計算結果を返す関数。普通に使うなら、calc(3, 1, ‘-‘)みたいな感じで使う。
例えば、プログラム中で引き算のみを使う需要しかないような場合があるとする。そんなときはfunctools.partialを使うことで引き算を行う関数minus(x, y)を
minus = functools.partial(calc, mode=’-‘)
とすることで定義できる。このminusは第一引数、第二引数に数値を受け取り引き算の結果を返す(calcの第三引数であるmodeが常に’-‘に固定されている)。

bottle.pyでは次のように使われている。urlunquote関数のencodingキーワード引数に’latin1’を部分適用して、urlunquote自身を上書きしている。これによって、encoding=’latin1’で固定された仕様のurlunquote関数を再定義している。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote
urlunquote = functools.partial(urlunquote, encoding='latin1')
---------------------
中略
---------------------
def _parse_qsl(qs):
    r = []
    for pair in qs.replace(';','&').split('&'):
        if not pair: continue
        nv = pair.split('=', 1)
        if len(nv) != 2: nv.append('')
        key = urlunquote(nv[0].replace('+', ' '))
        value = urlunquote(nv[1].replace('+', ' '))
        r.append((key, value))
    return r

2. functools.wraps

こいつはデコレータを書くときに使用する(それ以外にも効果的な使い方あるのかな?)。これを使わなくてもデコレータを書くことはできるんだけど、どうやらfunctools.wrapsを使って実装する方が良いようだ。というのも、こいつを使わずにデコレータを書くと、デコレート対象の関数のドキュメンテーション文字列が消えてしまう。こいつを使ってやると、デコレート対象関数のドキュメンテーション文字列が消えることはない。この挙動については公式ドキュメントにサンプルコードがある。また、こちらのブログの説明もわかりやすい。

そしてデコレータの復習。functools.wrapsを使ってデコレータを書いてみる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import functools

def ore(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print "@ore : oreoreoreoreore"
        return func(*args, **kwargs)
    return wrapper

def plus(x, y):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print "@plus(x, y) : %d" % (x + y)
            return func(*args, **kwargs)
        return wrapper
    return decorator

def after(tm):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            func(*args, **kwargs)
            import time
            print "@after(tm) : sleep %s seconds...." % tm
            time.sleep(tm)
        return wrapper
    return decorator

@ore
@plus(2, 5)
@after(5)

def main(n):
    """ main function """
    print n

if __name__ == '__main__':
    main('mainmainmainmain')
    print "__doc__ : %s" % main.__doc__

  • ore:引数なしのデコレータ。@oreでデコレートすると、デコレートされる関数が実行される前に、”@ore : oreoreoreoreore”が出力される
    • wrapperはデコレートされる関数の引数を受け取る。こいつを@functools.wraps(func)でデコレートする。funcはデコレートされる関数である。
  • plus(x, y):デコレータに引数ありのパターン。@plus(x, y)でデコレートすると、デコレートされる関数が実行される前に、”@plus(x, y) : XX”が出力される。XXにはxとyの足し算の結果が表示される。plusおよびその下にネストされた関数はそれぞれ次のような意味をもつ。
    • plus(x, y)はデコレータの実装自身。x, yはデコレータ自身の引数。
    • decorator(func)はデコレートされる関数を受け取る
    • wrapperはデコレートされる関数の引数を受け取る。こいつを@functools.wraps(func)でデコレートする。ま、oreと一緒だね。
  • after(tm): こいつはオマケ。ある関数実行時に事後処理を行わせたいときの例。@after(tm)でデコレートすると、デコレートされる関数が実行された後に、”@after(tm): sleep XXX seconds….”を出力し、「tm」で指定された秒数スリープする。
上記のサンプルコードを実行すると挙動が確認できる。

1
2
3
4
5
@ore : oreoreoreoreore
@plus(x, y) : 7
mainmainmainmain
@after(tm) : sleep 5 seconds....
__doc__ : main function

デコレート対象関数であるmain(n)の処理である「mainmainmainmain」の出力の前に出事前処理(@ore, @plus(x, y))が実行され、「mainmainmainmain」の出力後に5秒間のスリープ(@after(5))が入り、main()のドキュメンテーション文字列がちゃんと出力される

 

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <img localsrc="" alt="">

Set your Twitter account name in your settings to use the TwitterBar Section.