Python currying e parziale

Aug 19 2020

Durante gli esercizi di programmazione su codewars.com, mi sono imbattuto in un esercizio sul currying e sulle funzioni parziali.

Essendo un principiante nella programmazione e nuovo all'argomento, ho cercato su Internet informazioni sull'argomento e sono arrivato abbastanza lontano nella risoluzione dell'esercizio. Tuttavia ora mi sono imbattuto in un ostacolo che non riesco a superare e sto qui cercando una spinta nella giusta direzione.

L'esercizio è piuttosto semplice: scrivi una funzione che possa curry e / o parziale qualsiasi funzione di input e valuta la funzione di input una volta forniti sufficienti parametri di input. La funzione di input può accettare un numero qualsiasi di parametri di input. Anche la funzione curry / parziale dovrebbe essere molto flessibile nel modo in cui viene chiamata, essendo in grado di gestire molti, molti modi diversi di chiamare la funzione. Inoltre, la funzione curry / parziale può essere chiamata con più input di quelli richiesti dalla funzione di input, in tal caso tutti gli input in eccesso devono essere ignorati.

Seguendo il collegamento all'esercizio, è possibile trovare tutti i casi di test che la funzione deve essere in grado di gestire.

Il codice che mi è venuto in mente è il seguente:

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

Ora questo codice fallisce quando viene eseguito il seguente test:

test.assert_equals(curry_partial(curry_partial(curry_partial(add, a), b), c), sum)

dove add = a + b + c (funzione propriamente definita), a = 1, b = 2, c = 3 e sum = 6.

Il motivo per cui ciò non riesce è perché curry_partial(add, a)restituisce un handle di funzione alla funzione g. Nella seconda chiamata, curry_partial(<function_handle to g>, b)il calcolo minArgs = len(signature(func).parameters)non funziona come voglio, perché ora calcolerà quanti argomenti di input grichiede la funzione ( 1ovvero: ie *myArgs), e non quanti funcne richiede ancora l'originale . Quindi la domanda è: come posso scrivere il mio codice in modo da poter tenere traccia di quanti argomenti di input il mio originale ha funcancora bisogno (riducendo quel numero ogni volta che parzializzo la funzione con un dato argomento iniziale).

Ho ancora molto da imparare sulla programmazione e sul curry / parziale, quindi molto probabilmente non ho scelto l'approccio più conveniente. Ma mi piacerebbe imparare. La difficoltà in questo esercizio per me è la combinazione di parziale e curry, cioè fare un ciclo di curry mentre parzializzo tutti gli argomenti iniziali che si incontrano.

Risposte

1 Ava Aug 20 2020 at 13:27

Prova questo.

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

Questo in effetti non è molto buono in base alla progettazione. Invece dovresti usare class, per fare tutti i lavori interni come, ad esempio, il flip-flop (non preoccuparti, non abbiamo bisogno di alcun flip-flop lì ;-)).

Ogni volta che c'è una funzione che accetta argomenti arbitrari, puoi sempre istanziare quella classe passando la funzione. Ma questa volta, però, lo lascio a te.

Nishant Aug 21 2020 at 02:46

Non sono sicuro del curry , ma se hai bisogno di un semplice generatore di funzioni parziali, potresti provare qualcosa del genere:

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))

Anche se non risolve il tuo problema originale, vedi se puoi usare il codice sopra per ridurre la ripetizione del codice (non ne sono sicuro, ma penso che ci sia qualche ripetizione del codice nella funzione interna?); ciò renderà i problemi successivi più facili da risolvere.

Potrebbero esserci funzioni nella libreria standard che già risolvono questo problema. Molti linguaggi funzionali puri come Haskell hanno questa caratteristica incorporata nel linguaggio.