Pythonでの関数合成の活用法

この記事は広告を含みます。

こんにちは。今日もPython関数型プログラミングっぽい書き方について、紹介してゆきます。

前回は「同じ入力なら同じ結果を返す純粋関数」や map、filter を解説したけど、、、

kunio-ud-zatta.hatenablog.com

今回は「関数を引数に取ったり返したりする」高階関数や、reduce、partial、関数合成といった技も扱っていきます。

1. 高階関数って何?

高階関数って、名前の通り「関数を受け取ったり返したりする関数」のこと。 たとえば、こんな関数を作るとする:

def do_twice(func, x):
    """
    引数に渡された関数 func を、値 x に対して2回連続で適用する
    """
    return func(func(x))

ここでは、func はただの引数名です。Pythonには元から用意されてるわけじゃなくて、呼び出し時に渡される関数を受け取るための名前にすぎないです。

def add_one(n):
    return n + 1

result = do_twice(add_one, 5)  # 5 -> 6 -> 7 になる

2. 関数を返す関数

関数を返す関数も作れる。たとえば、ある数を足す関数を動的に作り出すには:

def make_adder(n):
    """
    n を足す関数を作って返す
    """
    def adder(x):
        return x + n
    return adder

add_five = make_adder(5)
print(add_five(10))  # => 15

この方法だと、必要に応じて「足す数」を動的に変えられる。面白いですね。

3. reduce を使ってデータをまとめる

reduce はリストの全要素を1つの値にまとめたいときに便利。 たとえば、リストの合計を計算する例はこう:

from functools import reduce

numbers = [1, 2, 3, 4, 5]
sum_all = reduce(lambda x, y: x + y, numbers)  # 結果は15

また、リストの最大値を求める場合もこんな感じで書ける:

numbers = [1, 7, 3, 9, 2]
max_val = reduce(lambda x, y: x if x > y else y, numbers)  # 結果は9

4. 部分適用で便利な関数を作る

functools.partial を使うと、一部の引数をあらかじめ固定した関数が作れる。 たとえば、累乗を計算する関数を部分適用で作ってみる

from functools import partial

def power(base, exp):
    return base ** exp

square = partial(power, exp=2)
cube   = partial(power, exp=3)

print(square(5))  # => 25
print(cube(2))    # => 8

5. 関数合成で関数をつなげる

数学の関数合成のように、1つの関数の出力を別の関数の入力に使うテクニック。 まずは自前で書く方法を!

def f(x):
    return x + 1

def g(x):
    return x * 2

def compose(f, g):
    """
    f のあとに g を実行する関数を返す
    (つまり、g(f(x)) を計算する関数)
    """
    def composite_function(x):
        return g(f(x))
    return composite_function

gf = compose(f, g)
print(gf(3))  # => 8  (3 + 1 = 4 で、4 * 2 = 8)

ちなみに、外部ライブラリの toolz を使うと、もっとスマートに書けるんだけど、今回は手作りの例で十分だと思う

6. まとめ

今回の章では、Python関数型プログラミングっぽい書き方を深掘りしてみた。

  • 高階関数:関数を引数に取ったり返したりする
  • reduce:リストの全要素をまとめる
  • partial:引数の一部を固定して新しい関数を作る
  • 関数合成:関数同士をつなげて新たな処理を作る

どれも、コードをすっきりさせたり、バグを減らすための有用なテクニックな気がする。

コードまとめ

# 高階関数:関数を引数として使う例
def do_twice(func, x):
    """
    引数に渡された関数 func を、値 x に対して2回連続で適用する
    """
    return func(func(x))

def add_one(n):
    return n + 1

result = do_twice(add_one, 5)
print("do_twice result:", result)  # 5 -> 6 -> 7

# 関数を返す関数の例
def make_adder(n):
    """
    n を足す関数を作って返す
    """
    def adder(x):
        return x + n
    return adder

add_five = make_adder(5)
print("make_adder result:", add_five(10))  # 10 + 5 => 15

# reduce を使った例
from functools import reduce

numbers = [1, 2, 3, 4, 5]
sum_all = reduce(lambda x, y: x + y, numbers)
print("reduce sum_all:", sum_all)  # 結果: 15

numbers = [1, 7, 3, 9, 2]
max_val = reduce(lambda x, y: x if x > y else y, numbers)
print("reduce max_val:", max_val)  # 結果: 9

# 部分適用 (partial) の例
from functools import partial

def power(base, exp):
    return base ** exp

square = partial(power, exp=2)
cube   = partial(power, exp=3)

print("square(5):", square(5))  # 結果: 25
print("cube(2):", cube(2))      # 結果: 8

# 関数合成の例
def f(x):
    return x + 1

def g(x):
    return x * 2

def compose(f, g):
    """
    f のあとに g を実行する関数を返す
    (つまり、g(f(x)) を計算する関数)
    """
    def composite_function(x):
        return g(f(x))
    return composite_function

gf = compose(f, g)
print("compose result:", gf(3))  # 結果: 8  (3 + 1 = 4, 4 * 2 = 8)