web-dev-qa-db-fra.com

Chaîne de fonctions en Python

Sur codewars.com, j'ai rencontré la tâche suivante:

Créez une fonction add qui ajoute des nombres lorsqu’elle est appelée successivement. Donc, add(1) devrait renvoyer 1, add(1)(2) devrait renvoyer 1+2, ...

Bien que je connaisse les bases de Python, je n’ai jamais rencontré de fonction pouvant être appelée dans une telle succession, c’est-à-dire une fonction f(x) pouvant être appelée f(x)(y)(z).... Jusqu'ici, je ne sais même pas comment interpréter cette notation. 

En tant que mathématicien, je soupçonne que f(x)(y) est une fonction qui attribue à chaque x une fonction g_{x}, puis renvoie g_{x}(y) et de même pour f(x)(y)(z)

Si cette interprétation était correcte, Python me permettrait de créer dynamiquement des fonctions qui me semblent très intéressantes. J'ai cherché sur le Web pendant une heure, mais je n'ai pas réussi à trouver une piste dans la bonne direction. Puisque je ne sais pas comment on appelle ce concept de programmation, cela n’est peut-être pas trop surprenant.

Comment appelez-vous ce concept et où puis-je en savoir plus à ce sujet?

79
Stefan Mesken

Je ne sais pas s'il s'agit de fonction chaînant autant que de appelable chaînant, mais, comme les fonctions sont callables, je suppose qu'il n'y a pas de mal à faire. De toute façon, je peux penser à cela de deux manières:

Sous-classement int et définissant __call__:

La première méthode consisterait en une sous-classe int personnalisée qui définit __call__ qui renvoie une nouvelle instance avec la valeur mise à jour:

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

La fonction add peut maintenant être définie pour renvoyer une instance CustomInt qui, en tant qu'appelable renvoyant une valeur mise à jour d'elle-même, peut être appelée successivement:

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

De plus, en tant que sous-classe int, la valeur renvoyée conserve le comportement __repr__ et __str__ de ints. Pour les opérations plus complexes, vous devez définir les autres dunders de manière appropriée}. 

Comme @Caridorc l'a noté dans un commentaire, add pourrait également être simplement écrit ainsi:

add = CustomInt 

Renommer la classe en add au lieu de CustomInt fonctionne également de la même manière.


Définir une fermeture, nécessite un appel supplémentaire pour générer une valeur:

Le seul autre moyen auquel je peux penser implique une fonction imbriquée qui nécessite un appel d'argument vide supplémentaire afin de renvoyer le résultat. J'utilise pas avec nonlocal et j'opte pour l'attachement d'attributs aux objets fonction pour le rendre portable entre Pythons:

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 

Ceci retourne continuellement lui-même (_inner_adder) qui, si une val est fournie, l'incrémente (_inner_adder += val) et sinon, renvoie la valeur telle quelle. Comme je l'ai mentionné, il faut un appel supplémentaire () pour renvoyer la valeur incrémentée:

>>> add(1)(2)()
3
>>> add(1)(2)(3)()  # and so on..
6
88

Vous pouvez me détester, mais voici un one-liner :)

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

Edit: Ok, comment ça marche? Le code est identique à la réponse de @Jim, mais tout se passe sur une seule ligne.

  1. type peut être utilisé pour construire de nouveaux types: type(name, bases, dict) -> a new type. Pour name, nous fournissons une chaîne vide, car le nom n'est pas vraiment nécessaire dans ce cas. Pour bases (Tuple), nous fournissons un (int,), identique à l'héritage de int. dict sont les attributs de classe, où nous attachons le __call__ lambda.
  2. self.__class__(self + v) est identique à return CustomInt(self + v)
  3. Le nouveau type est construit et renvoyé dans le lambda externe.
23
Jordan Jambazov

Si vous souhaitez définir une fonction à appeler plusieurs fois, vous devez tout d'abord renvoyer un objet appelable (par exemple, une fonction), sinon vous devez créer votre propre objet en définissant un attribut __call__, afin qu'il soit appelable. .

Le point suivant est que vous devez conserver tous les arguments, ce qui signifie dans ce cas que vous voudrez peut-être utiliser Coroutines ou une fonction récursive. Mais notez que Coroutines sont beaucoup plus optimisés/flexibles que les fonctions récursives, spécialement pour de telles tâches.

Voici un exemple de fonction utilisant Coroutines, qui conserve son dernier état. Notez qu'il ne peut pas être appelé plusieurs fois car la valeur de retour est une integer qui n'est pas appelable, mais vous pouvez envisager de le transformer en objet attendu ;-).

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
16
Kasrâmvd

La méthode pythonique consiste à utiliser des arguments dynamiques:

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

Ce n'est pas la réponse que vous cherchez, et vous le savez peut-être, mais je pensais pouvoir la donner quand même, car si quelqu'un se demandait si cela ne devait pas être fait par curiosité, mais par travail. Ils devraient probablement avoir la réponse "bonne à faire".

5
nichochar

Si vous souhaitez accepter un _()_ supplémentaire afin de récupérer le résultat, vous pouvez utiliser functools.partial :

_from functools import partial

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

Par exemple:

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

Cela permet également de spécifier plusieurs numéros à la fois:

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

Si vous souhaitez le limiter à un seul numéro, vous pouvez procéder comme suit:

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

Si vous voulez que add(x)(y)(z) renvoie facilement le résultat et que soit appelable, le sous-classement int est le chemin à parcourir.

1
a_guest

Simplement:

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