Pythonで2つ以上の関数合成


最近Haskellで関数型プログラミングの勉強を始めて、幸せになっているKekehoです。
関数合成というのができることを知って、これPythonでもやりたい!となったので早速実装していきましょう。

これといって難しいこともなく、シンプルに実装できます。

そもそも関数合成とは

数学でやったアレです。データxに対してf → gと関数を適用していきたいとき、

g(f(x))

って書きますよね。数学っぽく書くと、

g \circ f

になります。
Pythonで書くと、

# 事前に関数f, gは定義されているとする
f_to_g = lambda x: g(f(x))

ってなりますね。
これで引数xに関数をf→gという順番で適用させていき、その値を返してくれるという合成関数f_to_gができました。
嬉しくなっちゃいますね。

こんな感じで、手作業でいいならシンプルに書けてしまいます。記事が終わってしまいそうですね。

しんどい

しかし、もっとたくさん関数合成をしたい場合はどうすればいいでしょうか。
例えば、引数xに対してf→g→h→i→j...と関数を適用していきたい場合は?

もちろんlambda x: j(i(h(g(f(x))))と書けばいいだけです。
しかしながら、なんだか右から左に読んでいかないと適用順が把握できなくて直感的ではないですね。
更に、括弧が多すぎたりして読み書きがしづらいです。

アラビア語とLispが堪能な人材を雇う前に、関数合成をしてくれる関数を書くべきです。
↓こんな感じの関数を作りたいですね。

理想像
# j⚪︎i⚪︎h⚪︎g⚪︎f
composited = composite(f, g, h, i, j)  # 合成関数compositedを作成

実装例

再帰で実装してみました。
流石に1000個以上もの関数を合成する場面はまずないと思うのですが、もしやりたいなら

import sys
sys.setrecursionlimit(任意の数)  # 再帰のリミット数を設定(デフォルトは1000)

でいい感じに設定したほうがいいかもしれません。

以下に実装例を示しておきます。
分かりづらいところあったらコメントで指摘していただければ嬉しいです。

実装例
from typing import List
from types import FunctionType, LambdaType


def composite(*func: List[FunctionType]) -> LambdaType:
    if len(func) < 2:
        # エラー処理(流石に関数は2つ以上ないと合成もクソもない)
        raise TypeError(f'composite expected over 2 arguments, but got {len(func)}')
    if len(func) == 2:
        # 再帰の末尾
        return lambda *args, **kwargs: func[1](func[0](*args, **kwargs))

    # 再帰でぶん回す(((((((((((っ・ω・)っ ブーン
    return lambda *args, **kwargs: composite(*func[1:])(func[0](*args, **kwargs))

テスト

実際使ってみましょう。あくまでも例なので適当です。

試しに、人名をイニシャルに加工する合成関数を考えてみます。('taro', 'yamada')'T.Y'

ファーストネームとラストネームを受け取るデータとしましょう。 'taro', 'yamada'
heads関数でそれぞれ頭文字を取る ['t', 'y']dot_join関数で結合 't.y'str.upper関数で大文字に 'T.Y'
こんな感じで関数を適用していけばOKそうですね。

test.py
import composite # さっき書いたcomposite関数をいい感じにimportしておいてください

heads = lambda first, last: (first[0], last[0])
dot_join = lambda sequence: '.'.join(sequence)

name_to_initial = composite(heads, dot_join, str.upper)
print(name_to_initial('taro', 'yamada'))
$ python3 test.py
T.Y

やったね!

感想

Pythonは関数も第一級オブジェクトなので、簡単に実装できて嬉しかったです。