初心者がまとめるPython Programmingと時々数学

University of California Berkeleyに在籍中。自分用に勉強内容をまとめるブログ。

高階関数 Higher Order Functions

高階関数とは、別の関数を引数として渡して処理をしたり、戻り値として別の関数を返したりする関数のこと。

通常は引数や戻り値には数値などを入れたりして単純な結果を返していただけだったが、この概念を使うことでより複雑な処理もできるようになる。

そもそもなぜ関数を定義するか、それは関数を定義することで、値を渡すだけで一々手計算をしなくて済むからである。

このような考え方から、同じような事をする関数があったとき、それを一般化する関数を書くことができれば、詳細の機能を自由に変更できる。

 

例として、次の3つのプログラムを考える。

① 0からnまでの和を計算する関数

>>> def sum_naturals(n):
        total, k = 0, 1
        while k <= n:
            total, k = total + k, k + 1
        return total
>>> sum_naturals(100)
5050

② 0からnまでの2乗の和を計算する関数
>>> def sum_cubes(n):
        total, k = 0, 1
        while k <= n:
            total, k = total + k*k*k, k + 1
        return total
>>> sum_cubes(100)
25502500
 
③ nが増えるにつれて円周率πにだんだん収束していく数列の和を計算する関数
>>> def pi_sum(n):
        total, k = 0, 1
        while k <= n:
            total, k = total + 8 / ((4*k-3) * (4*k-1)), k + 1
        return total
>>> pi_sum(100)
3.1365926848388144

この3つの関数の作りはよく似ている。
違いは足していくものが整数なのか、整数を2乗したものなのか、そして分数なのかという事のみ。
この共通した部分だけを抜き出して一般化した関数を作る事で、別に整数を渡す関数、整数の2乗を渡す関数、そして分数を渡す関数を作ってそれを引数として渡す事で、一々関数を定義し直さなくても良くなる。

その共通部分を抜き出して一般化した関数がこちら。

def summation(n, term):
total, k = 0, 1
while k <= n:
total, k = total + <term>(k), k + 1
return total

変わった部分は引数にtermが増えてることと、totalに足されるものが<term>(k)となっている事。この<term>の部分に別の整数を渡す関数、整数の2乗を渡す関数、または分数を渡す関数を渡す事でそれぞれの和が計算できる。

整数を渡す関数
def identity(x):
return x

整数の2乗を渡す関数
def square(x):
return x * x

πに収束していく分数を渡す関数
def pi_term(x):
return 8 / ((4*x-3) * (4*x-1))

これで準備が整った。
10までの自然数の和を求めたい場合、
>>> summation(10, identity)
55

または何の和なのか分かりやすくするため新たに定義しても良い。
>>> def sum_natural(n):
return summation(n, identity)
>>> sum_natural(10)
55

同様に、2乗の和は、
>>> def sum_square(n):
return summation(n, square)
>>> sum_square(10)
385

πに収束していく分数の和は、
>>> def sum_pi(n):
return summation(n, pi_term)
>>> sum_pi(10)
3.091623806667838
>>> sum_pi(100)
3.1365926848388144

これが基本的な高階関数の使い方の1つである。
別の例として、図形の面積を求める関数も高階関数にする事で簡単に求められる。
一辺の長さがrの正方形、正六角形、または半径の長さがrの円の面積の求め方は、
正方形=r * r
正六角形=(3√3)/2 * r * r
円=π * r * r
この3つの共通点はrの2乗に係数をかけているという事。
なので、このrの2乗を共通点として一般化した関数を書き、新たに正方形の係数をかける関数、正六角形の係数をかける関数、そして円の係数をかける関数を書くことができる。

>>> from math import pi, sqrt # 正六角形で√、円でπが必要なため
>>>
>>> def area(r, shape_constant):
     assert r > 0, 'A length must be positive'
     return r * r * shape_constant

>>> def area_square(r):
     return area(r, 1)

>>> def area_hexagon(r):
return area(r, 3*sqrt(3)/2)

>>> def area_circle(r):
return area(r, pi)

これで面積を計算する準備ができたので早速実行してみる。

>>> area_square(10)
100
>>> area_hexagon(10)
259.8076211353316
>>> area_circle(10)
314.1592653589793
>>>

ここまでが関数を引数として渡した場合の例。
次に、関数の中に別の関数を定義した場合と、返り値に関数を渡した場合。
この場合のメリットとしては、関数内で定義された関数は、親の変数を使える事。デメリット?としては、親の関数の変数を書き換えることは出来ないので、入れ子の関数の中で変数に代入すると、親の関数の変数は隠されてしまう。また、定義された関数の中でのみ有効で、他の関数から呼び出すことは出来ない。

例として、ある条件を満たしているnまでの整数をプリントアウトする関数を考える。

>>> def keep_ints(n):
"""Returns a function which takes one parameter cond and
prints out all integers 1..i..n where calling cond(i)
returns True.
>>> def is_even(x):
return x % 2 == 0
>>> keep_ints(5)(is_even)
2
4
"""
def do_keep(cond):
i = 1
while i <= n:
if cond(i):
print(i)
i += 1
return do_keep

テストケースの>>> keep_ints(5)(is_even)に注目。
先にkeep_ints(5)を実行して、関数が返り、その関数にis_evenを渡していることが分かる。なので、keep_intsのreturn値は関数である。
関数do_keep内では、condに渡された関数が条件を満たすかどうかを判定(今回の場合is_even、偶数かどうかを判定)して満たしているならプリントアウトする。

なぜdo_keepをkeep_ints中に定義する必要があるのか、それはもしこのdo_keepをkeep_intsの外側、要は全く別の関数として定義した場合、keep_intsに渡したnの値がdo_keepの中では一切使えなくなってしまうためである(もしこのdo_keepが定義されたところと同じスコープ内でnが定義されているなら、そのnの値を使えるが、keep_intsのnとは異なるため、2つの関数は全く紐づいていないことになる)。

ちなみに、keep_ints(5)(is_even)のように、引数を1つ1つ渡していく関数をカリー化という。
他にも、カリー化のわかりやすい例として以下のようなものがある。

>>> def make_adder(n):
def adder(k):
return n + k
return adder

>>> make_adder(2)(3)
5
>>> make_adder(2)
<function make_adder.<locals>.adder at 0x10f7cbd90>

このように、make_adder(2)だけでは関数adderを返しているだけにすぎなく、次の引数(3)を渡して初めて計算がされて数値が返ってくる。

練習問題1
2つの関数f、gを引数とし、f(g(x))を計算する高階関数を書きなさい。
>>> def compose(f, g):
"""
>>> def square(x):
return x * x
     >>> comppose(square, square)(2)
16
""" # 以下解答
     def h(x):
       return f(g(x))
     return h

練習問題2
先ほどのcomposeを改良して、f(f(f...f(x)...)をn回繰り返す関数を書きなさい。
>>> def n_compose(n, f):
"""
     >>> n_compose(3, square)(2) # ((2**2)**2)**2

256
""" # 以下解答
def compose_helper(x):
i, result = 0, x
while i < n:
result = f(result)
i += 1
return result
return compose_helper