Chaînage de fonctions en Python

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

Créez une fonction add qui ajoute des nombres ensemble lorsqu'elle est appelée successivement. Donc add(1) devrait revenir 1, add(1)(2) doit retourner 1+2, ...

Bien que je connaisse les bases de Python, je n'ai jamais rencontré de fonction capable d'être appelée dans une telle succession, c'est-à-dire une fonction f(x) qui peut être appelée f(x)(y)(z).... Jusqu'à présent, je ne suis même pas sûr de savoir comment interpréter cette notation.

Comme un mathématicien, j'avais pense que f(x)(y) est une fonction qui assigne à chaque x une fonction g_{x}, puis retourne 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 la dernière heure, mais je n'ai pas pu trouver une piste dans la bonne direction. Puisque je ne sais pas comment ce concept de programmation est appelé, cependant, cela peut ne pas être trop surprenant.

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

74
demandé sur root 2016-08-19 14:48:30

5 réponses

Je ne sais pas si c'est function chaînage d'autant que c'est appelable chaînage, mais, étant donné que les fonctions sont callables je suppose qu'il n'y a pas de mal à cela. De toute façon, il y a deux façons de penser à faire ceci:

Sous-classe int et définition __call__:

La première façon serait avec une sous-classe int personnalisée qui définit __call__ qui renvoie une nouvelle instance de lui-même avec la valeur mise à jour:

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

Fonction add peut maintenant être défini pour renvoyer une instance CustomInt, qui, en tant qu'appelable qui renvoie une valeur mise à jour de lui-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

En outre, en tant que sous-classe int, la valeur renvoyée conserve le comportement __repr__ et __str__ de int. pour des opérations plus complexes, vous devez définir d'autres dunders de manière appropriée.

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

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 yield value:

La seule autre façon dont je peux penser implique une fonction imbriquée qui nécessite un appel d'argument vide supplémentaire afin de retourner le résultat. Je suis Pas en utilisant nonlocal et j'opte pour attacher des attributs aux objets de 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 

Cela se retourne en continu (_inner_adder) qui, si un val est fourni, l'incrémente (_inner_adder += val) et si ce n'est pas le cas, renvoie la valeur telle quelle. Comme je l'ai mentionné, il nécessite un appel () supplémentaire afin de retourner la valeur incrémentée:

>>> add(1)(2)()
3
>>> add(1)(2)(3)()  # and so on..
6
86
répondu Jim Fasarakis Hilliard 2016-10-02 18:48:46

Vous pouvez me haïr, mais voici un one-liner:)

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

Edit: Ok, comment cela fonctionne? 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,), qui est identique à l'héritage 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
répondu Jordan Jambazov 2016-08-21 22:50:54

Si vous souhaitez définir une fonction est appelée plusieurs fois, d'abord vous avez besoin de retourner un objet appelable à chaque fois (par exemple une fonction) sinon, vous devez créer votre propre objet en définissant un __call__ attribut, pour être appelable.

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

Voici un exemple de fonction utilisant Coroutines, qui préserve le dernier état de lui-même. Notez qu'il ne peut pas être appelé plusieurs fois car la valeur de retour est un integer qui n'est pas appelable, mais vous pourriez penser à transformer cela en votre 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
répondu Kasrâmvd 2016-08-20 18:42:28

La façon pythonique de le faire serait d'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 j'ai pensé que je la donnerais de toute façon parce que si quelqu'un s'interrogeait sur le fait de le faire pas par curiosité mais pour le travail. Ils devraient probablement avoir la réponse" bonne chose à faire".

5
répondu nichochar 2016-08-25 15:26:09

Simplement:

class add(int):
   def __call__(self, n):
      return add(self + n)
0
répondu Nicolae 2018-05-10 07:48:45