LoginSignup
24
15

More than 5 years have passed since last update.

高階関数とデコレータ

Last updated at Posted at 2016-11-20

関数のちょっと偉い版である高階関数と Python のデコレータ記法について説明する。

関数

集合Aと集合Bがあるとき、集合Aの任意の元 a に対し、集合Bの元 f(a) を一意に決める規則 f を、 A から B への関数という。

引数を2倍して返す関数 times2 を考える

def times2(i):
    return i * 2

このとき、 i として 0, 1, 2 を与えると

  • times2(0) == 0
  • times2(1) == 2
  • times2(2) == 4

set([0,1,2]) から整数の集合への関数が定義されている。

times2.png

プログラミングにおける関数

プログラミングにおいて関数には以下のような効能がある

  • 同じようなコードがあちこちにある→抽出して1か所にまとめる→知識が集約される
  • ひとまとめにできるコードの断片がある→目的を表す名前をつける→処理を俯瞰できる

以下で説明する高階関数を使いこなすことで、これらの効能を得られる場面が増える。

引数としての関数

引数を0倍する関数、1倍する関数、2倍する関数があったとき、それらに同じ値 1 を適用するという処理を考える。

  • times0(1) == 0
  • times1(1) == 1
  • times2(1) == 2

先程の関数 times2: set([0, 1, 2]) -> Int の定義と見比べると、関数の集合 set([times0, times1, times2]) から整数の集合への対応規則が見えてくる。

  • apply1(times0) == 0
  • apply1(times1) == 1
  • apply1(times2) == 2

apply1.png

apply1 は以下のように実装できる

def apply1(f):
    return f(1)

このように関数を引数とする関数(あるいは次の節で見るような関数を返す関数)のことを高階関数と呼ぶ。

戻り値としての関数

数を引数に取り、関数を返す高階関数

def times_n(n):
    def f(i):
        return n * i
    return f

を使うと先程出てきた times0, times1, times2

times0 = times_n(0)
times1 = times_n(1)
times2 = times_n(2)

と定義することができる。

times_n.png

>>> apply1(times0)
0
>>> apply1(times1)
1
>>> apply1(times2)
2

関数の集合から関数の集合への関数

関数を引数に取り、関数を返す高階関数を使うと

  • 関数の引数を前処理する新たな関数をつくる
  • 関数の戻り値を後処理する新たな関数をつくる
  • 関数の前後に処理を差し込む
    • 実行時間の計測
    • 呼び出しの記録

などを再利用可能な形で書くことができる。

def dot(g):
    "f -> g . f"
    def func(f):
        def composite(i):
            return g(f(i))
        return composite
    return func
>>> (lambda i: i*2)((lambda i: i+5)(1))
12
>>> f = dot(lambda i: i*2)(lambda i: i+5)
>>> f(1)
12

デコレータ記法

関数を引数に取り、関数を返す高階関数 decorator があるとき

@decorator
def function(argument):
    # ...

と書くと functiondecorator(function) で置き換えてくれる。

  • function を定義した後に function = decorator(function) するのと挙動は同じ
  • 関数定義と関数の置き換えが同じブロックに書けるので可読性が高い

デコレータの例

関数呼び出しのログをプリントするデコレータを書いて、効率の悪い再帰関数に適用してみる。

def trace(function):
    "呼び出しログをプリントするデコレータ"
    def inner(*args):
        "function の前後で呼び出しログをプリントする"
        print("{0}{1}".format(function.__name__, args))
        ret = function(*args)
        print("{0}{1} ==> {2}".format(function.__name__, args, ret))
        return ret
    return inner


@trace
def fib(n):
    "フィボナッチ数を求める"
    if n == 0:
        return 0
    if n == 1:
        return 1
    return fib(n-2) + fib(n-1)

同じ計算を何度もしてしまっているのが見れる。

% python3 -c 'import fib; fib.fib(4)'
fib(4,)
fib(2,)
fib(0,)
fib(0,) ==> 0
fib(1,)
fib(1,) ==> 1
fib(2,) ==> 1
fib(3,)
fib(1,)
fib(1,) ==> 1
fib(2,)
fib(0,)
fib(0,) ==> 0
fib(1,)
fib(1,) ==> 1
fib(2,) ==> 1
fib(3,) ==> 2
fib(4,) ==> 3

TIPS: 属性の引き継ぎ

先程の例だと fib の docstirng, 関数名は inner のものになってしまう。

>>> fib.__doc__
'function の前後で呼び出しログをプリントする'
>>> fib.__name__
'inner'

→ 複数の関数に同じデコレータを適用したときに辛い

from functools import wraps


def trace(function):
    "呼び出しログをプリントするデコレータ"
    @wraps(function)
    def inner(*args):
        "function の前後で呼び出しログをプリントする"
    ...

としておくと

>>> fib.__doc__
'フィボナッチ数を求める'
>>> fib.__name__
'fib'

と元の関数の docstring と名前を引き継いでくれる。
スタックトレースを表示したときに役に立つ

TIPS: *args**kwargs

*args**kwargs を使うとより汎用的なデコレータを書ける。

TIPS: 置き換えない

デコレータでは必ずしも関数を新たな関数に置き換える必要はない。例えば

  • 条件に合致した場合のみ置き換える (e.g. unittest.skipIf)
  • setattr で属性を付けて、元の関数を返す

なども可能。

まとめ

  • 関数を引数に取ったり、戻り値が関数な関数を高階関数という
  • 値に対する手続きをまとめる → 手続きに対する手続きをまとめる
  • デコレータ記法で関数の置き換えを簡潔に書ける
24
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
15