Python currying e parziale
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 g
richiede la funzione ( 1
ovvero: ie *myArgs
), e non quanti func
ne 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 func
ancora 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
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.
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.