Encadenamiento de funciones en Python

90

En Codewars.com encontré la siguiente tarea:

Cree una función addque sume números cuando se llame en sucesión. Entonces add(1)debería volver 1, add(1)(2)debería volver 1+2, ...

Si bien estoy familiarizado con los conceptos básicos de Python, nunca he encontrado una función que se pueda llamar en tal sucesión, es decir, una función f(x)que se pueda llamar como f(x)(y)(z).... Hasta ahora, ni siquiera estoy seguro de cómo interpretar esta notación.

Como matemático, sospecho que f(x)(y)es una función que asigna a cada xuna función g_{x}y luego devuelve g_{x}(y)y lo mismo para f(x)(y)(z).

Si esta interpretación fuera correcta, Python me permitiría crear dinámicamente funciones que me parecen muy interesantes. Busqué en la web durante la última hora, pero no pude encontrar una pista en la dirección correcta. Sin embargo, como no sé cómo se llama este concepto de programación, esto puede no ser demasiado sorprendente.

¿Cómo se llama a este concepto y dónde puedo leer más sobre él?

Stefan Mesken
fuente
7
Parece que está buscando funciones de curry
OneCricketeer
2
Sugerencia: una función anidada se crea dinámicamente, tiene acceso a los valores locales de su función principal y se puede devolver como un objeto (invocable).
Jonathon Reinhart
@JonathonReinhart Esa es la forma en que pensaba sobre el problema. Pero realmente no vi cómo implementarlo.
Stefan Mesken
3
Como nota al margen: Python definitivamente le permitirá crear funciones dinámicamente. Si está interesado, aquí hay un par de conceptos relacionados para leer: WP: Funciones de primera clase | ¿Cómo se hace una función de orden superior en Python? | functools.partial()| WP: Cierres
Lukas Graf
@LukasGraf Le echaré un vistazo. ¡Gracias!
Stefan Mesken

Respuestas:

100

No sé si esto es tanto un encadenamiento de funciones como un encadenamiento invocable , pero, dado que las funciones son invocables, supongo que no hay ningún daño. De cualquier manera, hay dos formas en las que puedo pensar en hacer esto:

Subclasificación inty definición __call__:

La primera forma sería con una intsubclase personalizada que define __call__cuál devuelve una nueva instancia de sí mismo con el valor actualizado:

class CustomInt(int):
    def __call__(self, v):
        return CustomInt(self + v)

La función addahora se puede definir para devolver una CustomIntinstancia, que, como un invocable que devuelve un valor actualizado de sí mismo, se puede llamar en sucesión:

>>> def add(v):
...    return CustomInt(v)
>>> add(1)
1
>>> add(1)(2)
3
>>> add(1)(2)(3)(44)  # and so on..
50

Además, como una intsubclase, el valor devuelto conserva el comportamiento __repr__y __str__de ints. Sin embargo, para operaciones más complejas, debe definir otros dunders de manera apropiada .

Como señaló @Caridorc en un comentario, addtambién podría escribirse simplemente como:

add = CustomInt 

Cambiar el nombre de la clase a en addlugar de CustomInttambién funciona de manera similar.


Definir un cierre, requiere una llamada adicional para generar valor:

La única otra forma en la que puedo pensar involucra una función anidada que requiere una llamada de argumento vacía adicional para devolver el resultado. Estoy sin usar nonlocaly optar por fijar los atributos de los objetos de función para que sea portable entre pitones:

def add(v):
    def _inner_adder(val=None):  
        """ 
        if val is None we return _inner_adder.v 
        else we increment and return ourselves
        """
        if val is None:    
            return _inner_adder.v
        _inner_adder.v += val
        return _inner_adder
    _inner_adder.v = v  # save value
    return _inner_adder 

Esto se devuelve continuamente a sí mismo ( _inner_adder) que, si valse proporciona a, lo incrementa ( _inner_adder += val) y si no, devuelve el valor tal como está. Como mencioné, requiere una ()llamada adicional para devolver el valor incrementado:

>>> add(1)(2)()
3
>>> add(1)(2)(3)()  # and so on..
6
Dimitris Fasarakis Hilliard
fuente
6
En el código interactivo add = CostumIntdebería funcionar también y ser más sencillo.
Caridorc
4
El problema con las subclases de elementos integrados es que (2*add(1)(2))(3)falla con un TypeErrorporque intno se puede llamar. Básicamente, CustomIntse convierte a simple intcuando se usa en cualquier contexto, excepto al llamar. Para una solución más robusta, básicamente tiene que volver a implementar todos los __*__métodos, incluidas las __r*__versiones ...
Bakuriu
@Caridorc O no lo llames CustomInten absoluto sino addal definirlo.
minipif
27

Puedes odiarme, pero aquí hay una frase :)

add = lambda v: type("", (int,), {"__call__": lambda self, v: self.__class__(self + v)})(v)

Editar: Ok, ¿cómo funciona esto? El código es idéntico a la respuesta de @Jim, pero todo sucede en una sola línea.

  1. typepuede ser utilizado para construir nuevos tipos: type(name, bases, dict) -> a new type. Porque nameproporcionamos una cadena vacía, ya que el nombre no es realmente necesario en este caso. Para bases(tupla) proporcionamos un (int,), que es idéntico a heredar int. dictson los atributos de la clase, donde adjuntamos el __call__lambda.
  2. self.__class__(self + v) es idéntico a return CustomInt(self + v)
  3. El nuevo tipo se construye y se devuelve dentro de la lambda externa.
Jordan Jambazov
fuente
17
O incluso más corto:class add(int):__call__ = lambda self, v: add(self+v)
Bakuriu
3
El código dentro de una clase se ejecuta exactamente como el código normal, por lo que puede definir métodos especiales por asignaciones. La única diferencia es que el alcance de la clase es un poco ... peculiar.
Bakuriu
16

Si desea definir una función para que se llame varias veces, primero debe devolver un objeto invocable cada vez (por ejemplo, una función); de lo contrario, debe crear su propio objeto definiendo un __call__atributo para que sea invocable.

El siguiente punto es que debe conservar todos los argumentos, lo que en este caso significa que es posible que desee utilizar Coroutines o una función recursiva. Pero tenga en cuenta que las corrutinas son mucho más optimizadas / flexibles que las funciones recursivas , especialmente para tales tareas.

Aquí hay una función de muestra que usa Coroutines, que conserva el último estado de sí misma. Tenga en cuenta que no se puede llamar varias veces ya que el valor de retorno es uninteger , pero podría pensar en convertir esto en su objeto esperado ;-).

def add():
    current = yield
    while True:
        value = yield current
        current = value + current


it = add()
next(it)
print(it.send(10))
print(it.send(2))
print(it.send(4))

10
12
16
Kasravnd
fuente
6

La forma pitónica de hacer esto sería usar argumentos dinámicos:

def add(*args):
    return sum(args)

Esta no es la respuesta que estás buscando, y es posible que lo sepas, pero pensé que te la daría de todos modos porque si alguien se preguntaba por hacer esto no por curiosidad sino por trabajo. Probablemente deberían tener la respuesta de "lo correcto".

nichochar
fuente
1
Eliminé tu nota ' PS ', nichochar. Todos somos conscientes de lo elegante que es Python :-) No creo que pertenezca al cuerpo de la respuesta.
Dimitris Fasarakis Hilliard
6
Creo que podrías haberlo hecho add = sumsi
hubieras
3

Simplemente:

class add(int):
   def __call__(self, n):
      return add(self + n)
Nicolae
fuente
3

Si está dispuesto a aceptar un adicional ()para recuperar el resultado, puede usar functools.partial:

from functools import partial

def add(*args, result=0):
    return partial(add, result=sum(args)+result) if args else result

Por ejemplo:

>>> add(1)
functools.partial(<function add at 0x7ffbcf3ff430>, result=1)
>>> add(1)(2)
functools.partial(<function add at 0x7ffbcf3ff430>, result=3)
>>> add(1)(2)()
3

Esto también permite especificar varios números a la vez:

>>> add(1, 2, 3)(4, 5)(6)()
21

Si desea restringirlo a un solo número, puede hacer lo siguiente:

def add(x=None, *, result=0):
    return partial(add, result=x+result) if x is not None else result

Si desea add(x)(y)(z)devolver fácilmente el resultado y poder llamar más, entonces la subclasificación intes el camino a seguir.

un invitado
fuente