Python curry y parcial
Mientras hacía ejercicios de programación en codewars.com, encontré un ejercicio sobre funciones parciales y de curado.
Siendo un novato en programación y nuevo en el tema, busqué en Internet información sobre el tema y llegué bastante lejos en la resolución del ejercicio. Sin embargo, ahora me he topado con un obstáculo que parece que no puedo superar y estoy aquí buscando un empujón en la dirección correcta.
El ejercicio es bastante simple: escriba una función que pueda curry y / o parcializar cualquier función de entrada y evalúe la función de entrada una vez que se hayan proporcionado suficientes parámetros de entrada. La función de entrada puede aceptar cualquier número de parámetros de entrada. Además, la función curry / parcial debe ser muy flexible en cómo se llama, pudiendo manejar muchas, muchas formas diferentes de llamar a la función. Además, se permite llamar a la función curry / parcial con más entradas de las requeridas por la función de entrada, en ese caso, se deben ignorar todas las entradas en exceso.
Siguiendo el enlace del ejercicio, se pueden encontrar todos los casos de prueba que la función debe poder manejar.
El código que se me ocurrió es el siguiente:
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
Ahora este código falla cuando se ejecuta la siguiente prueba:
test.assert_equals(curry_partial(curry_partial(curry_partial(add, a), b), c), sum)
donde sumar = a + b + c (función correctamente definida), a = 1, b = 2, c = 3 y suma = 6.
La razón por la que esto falla es porque curry_partial(add, a)
devuelve un identificador de función a la función g
. En la segunda llamada, curry_partial(<function_handle to g>, b)
el cálculo minArgs = len(signature(func).parameters)
no funciona como yo quiero, porque ahora calculará cuántos argumentos de entrada g
requiere la función (que es 1
: es decir *myArgs
), y no cuántos func
aún requiere el original . Entonces, la pregunta es, ¿cómo puedo escribir mi código de manera que pueda realizar un seguimiento de cuántos argumentos de entrada func
aún necesita mi original (reduciendo ese número cada vez que participo la función con cualquier argumento inicial dado)?
Todavía tengo mucho que aprender sobre programación y curry / parcial, así que lo más probable es que no haya elegido el enfoque más conveniente. Pero me gustaría aprender. La dificultad de este ejercicio para mí es la combinación de parcial y curry, es decir, hacer un bucle de curry mientras participo cualquier argumento inicial que se encuentre.
Respuestas
Probar esto.
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
De hecho, esto no es muy bueno por diseño. En su lugar, debería usar class
, para hacer todos los trabajos internos como, por ejemplo, el flip-flop (no se preocupe, no necesitamos ningún flip-flop allí ;-)).
Siempre que haya una función que tome argumentos arbitrarios, siempre puede instanciar esa clase pasando la función. Pero esta vez, sin embargo, te dejo eso.
No estoy seguro de curry , pero si necesita un generador de funciones parciales simple, puede probar algo como esto:
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))
Incluso si no resuelve su problema original, vea si puede usar el código anterior para reducir la repetición del código (no estoy seguro, pero creo que hay alguna repetición de código en la función interna); que facilitará la resolución de los problemas posteriores.
Podría haber funciones en la biblioteca estándar que ya resuelvan este problema. Muchos lenguajes funcionales puros como Haskell tienen esta característica incorporada en el lenguaje.