0554: シグネチャによる自動カリー化¶
問題¶
カリー化とは、多引数関数を、引数を1つずつ受け取る関数の連鎖に変換する操作です。 集合の言葉では、\(f : X \times Y \to Z\) を \(\mathrm{curry}(f) : X \to (Y \to Z)\) に対応させる全単射 \(Z^{X \times Y} \cong (Z^Y)^X\) にあたります。 Haskell などの関数型言語では、すべての関数が最初からカリー化されており、引数の一部だけを与える部分適用が自然に書けます。
デコレータ curry を実装してください。
@curryを付けた関数は、必要な位置引数がすべて揃うまで、部分適用された関数を返し続けます。- 引数は1個ずつでも複数個まとめてでも渡せます。渡した位置引数の総数が仮引数の個数に達した時点で、元の関数を呼び出して結果を返します。
- 仮引数の個数は
inspect.signature()で取得します。 functools.wrapsを使い、元の関数のメタデータを引き継ぎます。
制約¶
- 対象関数の仮引数は位置引数のみとし、デフォルト値、可変長引数(
*args、**kwargs)、キーワード専用引数は扱いません。 - 対象関数の仮引数は1個以上とします。
- カリー化後の関数はキーワード引数では呼び出されないものとします。
- 位置引数を仮引数の個数より多く渡した場合は
TypeErrorを送出します(元の関数の呼び出しで自然に発生するもので構いません)。 - 部分適用しても、元の関数や既存の部分適用の状態は変化しないものとします(下の例の
add_1_2を参照)。
例¶
>>> @curry
... def add3(x, y, z):
... return x + y + z
>>> add3(1)(2)(3)
6
>>> add3(1, 2)(3)
6
>>> add3(1)(2, 3)
6
>>> add3(1, 2, 3)
6
>>> add_1_2 = add3(1, 2)
>>> add_1_2(10)
13
>>> add_1_2(100) # 部分適用は何度でも再利用できる
103
>>> add3.__name__
'add3'
>>> volume = curry(lambda a, b, c: a * b * c) # デコレータ構文を使わない適用
>>> volume(2)(3)(4)
24
>>> add3(1, 2, 3, 4)
Traceback (most recent call last):
...
TypeError: ...
発展¶
デフォルト値やキーワード引数を持つ関数にも対応するように curry を拡張してください。
inspect.signature() の Parameter.kind と Parameter.default を調べ、必須の位置引数が揃った時点で元の関数を呼び出すようにします。
参考¶
- 『Python Distilled』第5章「イントロスペクション、属性、シグネチャ」
- 『Python Distilled』第5章「デコレータ」
- カリー化 - Wikipedia