Pythonカリー化と部分的

Aug 19 2020

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まだ必要な数を追跡できるようにコードをどのように記述できるかです(指定された初期引数で関数を部分化するたびにその数を減らします)。

私はまだプログラミングとカリー化/部分について学ぶことがたくさんあるので、おそらく最も便利なアプローチを選択していません。しかし、私は学びたいです。私にとってこの演習の難しさは、パーシャルとカレーの組み合わせです。つまり、遭遇した最初の引数をパーシャルしながらカレーループを実行します。

回答

1 Ava Aug 20 2020 at 13:27

これを試してみてください。

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、たとえばフリップフロップのようなすべての内部作業を行う必要があります(フリップフロップが必要ないことを心配しないでください;-))。

任意の引数を取る関数があるときはいつでも、関数を渡すそのクラスをいつでもインスタンス化できます。しかし、今回はそれをあなたに任せます。

Nishant Aug 21 2020 at 02:46

カリー化についてはよくわかりませんが、単純な部分関数発生器が必要な場合は、次のようなものを試すことができます。

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のような多くの純粋な関数型言語には、この機能が言語に組み込まれています。