Pythonカリー化と部分的
codewars.comでプログラミング演習を行っているときに、カリー化と部分関数に関する演習に遭遇しました。
プログラミングの初心者であり、トピックに不慣れだったので、インターネットでトピックに関する情報を検索し、演習の解決にかなり踏み込みました。しかし、私は今、克服できないと思われる障害に遭遇し、正しい方向への微調整を探しています。
演習はかなり単純です。任意の入力関数をカレーおよび/または部分化できる関数を記述し、十分な入力パラメーターが指定されると入力関数を評価します。入力関数は、任意の数の入力パラメーターを受け入れることができます。また、カレー/部分関数は、呼び出される方法が非常に柔軟であり、関数を呼び出すためのさまざまな方法を処理できる必要があります。また、カレー/部分関数は、入力関数で必要とされるよりも多くの入力で呼び出すことができます。その場合、余分な入力はすべて無視する必要があります。
演習のリンクをたどると、関数が処理できる必要があるすべてのテストケースが見つかります。
私が思いついたコードは次のとおりです。
from functools import partial
from inspect import signature
def curry_partial(func, *initial_args):
""" Generates a 'curried' version of a function. """
# Process any initial arguments that where given. If the number of arguments that are given exceeds
# minArgs (the number of input arguments that func needs), func is evaluated
minArgs = len(signature(func).parameters)
if initial_args:
if len(initial_args) >= minArgs:
return func(*initial_args[:minArgs])
func = partial(func, *initial_args)
minArgs = len(signature(func).parameters)
# Do the currying
def g(*myArgs):
nonlocal minArgs
# Evaluate function if we have the necessary amount of input arguments
if minArgs is not None and minArgs <= len(myArgs):
return func(*myArgs[:minArgs])
def f(*args):
nonlocal minArgs
newArgs = myArgs + args if args else myArgs
if minArgs is not None and minArgs <= len(newArgs):
return func(*newArgs[:minArgs])
else:
return g(*newArgs)
return f
return g
次のテストを実行すると、このコードは失敗します。
test.assert_equals(curry_partial(curry_partial(curry_partial(add, a), b), c), sum)
ここで、add = a + b + c(適切に定義された関数)、a = 1、b = 2、c = 3、およびsum = 6です。
これが失敗する理由curry_partial(add, a)
は、関数ハンドルを関数に返すためg
です。2番目の呼び出しでcurry_partial(<function_handle to g>, b)
は、計算minArgs = len(signature(func).parameters)
が希望どおりに機能しません。g
これは、元の関数がまだ必要とする数ではなく、関数が必要とする入力引数の数(1
つまり*myArgs
、:ie )を計算するfunc
ためです。したがって、問題は、元の入力引数がfunc
まだ必要な数を追跡できるようにコードをどのように記述できるかです(指定された初期引数で関数を部分化するたびにその数を減らします)。
私はまだプログラミングとカリー化/部分について学ぶことがたくさんあるので、おそらく最も便利なアプローチを選択していません。しかし、私は学びたいです。私にとってこの演習の難しさは、パーシャルとカレーの組み合わせです。つまり、遭遇した最初の引数をパーシャルしながらカレーループを実行します。
回答
これを試してみてください。
from inspect import signature
# Here `is_set` acts like a flip-flop
is_set = False
params = 0
def curry_partial(func, *partial_args):
"""
Required argument: func
Optional argument: partial_args
Return:
1) Result of the `func` if
`partial_args` contains
required number of items.
2) Function `wrapper` if `partial_args`
contains less than the required
number of items.
"""
global is_set, params
if not is_set:
is_set = True
# if func is already a value
# we should return it
try: params = len(signature(func).parameters)
except: return func
try:
is_set = False
return func(*partial_args[:params])
except:
is_set = True
def wrapper(*extra_args):
"""
Optional argument: extra_args
Return:
1) Result of the `func` if `args`
contains required number of
items.
2) Result of `curry_partial` if
`args` contains less than the
required number of items.
"""
args = (partial_args + extra_args)
try:
is_set = False
return func(*args[:params])
except:
is_set = True
return curry_partial(func, *args)
return wrapper
これは実際、設計上あまり良くありません。代わりに、を使用してclass
、たとえばフリップフロップのようなすべての内部作業を行う必要があります(フリップフロップが必要ないことを心配しないでください;-))。
任意の引数を取る関数があるときはいつでも、関数を渡すそのクラスをいつでもインスタンス化できます。しかし、今回はそれをあなたに任せます。
カリー化についてはよくわかりませんが、単純な部分関数発生器が必要な場合は、次のようなものを試すことができます。
from functools import partial
from inspect import signature
def execute_or_partial(f, *args):
max = len(signature(f).parameters)
if len(args) >= max:
return f(*args[:max])
else:
return partial(f, *args)
s = lambda x, y, z: x + y + z
t = execute_or_partial(s, 1)
u = execute_or_partial(t, 2)
v = execute_or_partial(u, 3)
print(v)
or
print(execute_or_partial(execute_or_partial(execute_or_partial(s, 1), 2), 3))
元の問題が解決しない場合でも、上記のコードを使用してコードの繰り返しを減らすことができるかどうかを確認してください(わかりませんが、内部関数にコードの繰り返しがあると思いますか?)。これにより、その後の問題の解決が容易になります。
この問題をすでに解決している関数が標準ライブラリにある可能性があります。Haskellのような多くの純粋な関数型言語には、この機能が言語に組み込まれています。