Chaînage de fonctions en Python

90

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

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

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

En tant que mathématicien, je soupçonne que f(x)(y)c'est une fonction qui assigne à chaque xfonction une g_{x}puis retourneg_{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 au cours de la dernière heure, mais je n'ai pas été en mesure de trouver une piste dans la bonne direction. Puisque je ne sais pas comment ce concept de programmation est appelé, cela n'est peut-être pas trop surprenant.

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

Stefan Mesken
la source
7
On dirait que vous recherchez des fonctions de curry
OneCricketeer
2
Conseil: une fonction imbriquée est créée dynamiquement, a accès aux paramètres locaux de sa fonction parent et peut être renvoyée en tant qu'objet (appelable).
Jonathon Reinhart
@JonathonReinhart C'est ainsi que je pensais au problème. Mais je n'ai pas vraiment vu comment le mettre en œuvre.
Stefan Mesken
3
En aparté: Python certainement vous permettre de créer dynamiquement des fonctions. Si vous êtes intéressé, voici quelques concepts connexes à lire: WP: Fonctions de première classe | Comment créer une fonction d'ordre supérieur en Python? | functools.partial()| WP: Closures
Lukas Graf
@LukasGraf Je vais y jeter un coup d'œil. Merci!
Stefan Mesken

Réponses:

100

Je ne sais pas si c'est autant le chaînage de fonctions que le chaînage appelable , mais comme les fonctions sont appelables, je suppose qu'il n'y a pas de mal. Quoi qu'il en soit, il y a deux façons dont je peux penser à cela:

Sous-classification intet définition __call__:

La première méthode serait avec une intsous-classe personnalisée qui définit __call__laquelle renvoie une nouvelle instance d'elle-même avec la valeur mise à jour:

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

La fonction addpeut maintenant être définie pour renvoyer une CustomIntinstance, qui, en tant qu'appelable qui renvoie 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 intsous-classe, 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, cela addpourrait également être simplement écrit comme suit :

add = CustomInt 

Renommer la classe en au addlieu de CustomIntfonctionne également de la même manière.


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

Le seul autre moyen auquel je puisse penser implique une fonction imbriquée qui nécessite un appel d'argument vide supplémentaire afin de renvoyer le résultat. Je n'utilise pasnonlocal 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 retourne continuellement lui-même ( _inner_adder) qui, si a valest fourni, l'incrémente ( _inner_adder += val) et sinon, retourne la valeur telle qu'elle est. Comme je l'ai mentionné, il nécessite un ()appel supplémentaire pour renvoyer la valeur incrémentée:

>>> add(1)(2)()
3
>>> add(1)(2)(3)()  # and so on..
6
Dimitris Fasarakis Hilliard
la source
6
Dans le code interactif add = CostumIntdevrait fonctionner aussi et être plus simple.
Caridorc
4
Le problème avec le sous-classement intégré est qu'il (2*add(1)(2))(3)échoue avec un TypeErrorcar intn'est pas appelable. Fondamentalement, le CustomIntest converti en plain intlorsqu'il est utilisé dans n'importe quel contexte, sauf lors de l'appel. Pour une solution plus robuste, vous devez essentiellement __*____r*__
réimplémenter
@Caridorc Ou ne l'appelez pas CustomIntdu tout mais addlors de sa définition.
minipif
27

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. typepeut être utilisé pour construire de nouveaux types: type(name, bases, dict) -> a new type. Car namenous 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. dictsont 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.
Jordanie Jambazov
la source
17
Ou encore plus court:class add(int):__call__ = lambda self, v: add(self+v)
Bakuriu
3
Le code à l'intérieur d'une classe est exécuté exactement comme le code normal afin que vous puissiez définir des méthodes spéciales par affectations. La seule différence est que la portée de la classe est un peu ... particulière.
Bakuriu
16

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

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

Voici un exemple de fonction utilisant Coroutines, qui préserve le dernier état d'elle-même. Notez qu'il ne peut pas être appelé plusieurs fois car la valeur de retour est un integerqui n'est pas appelable, mais vous pourriez penser à le transformer 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
Kasravnd
la source
6

La manière pythonique de faire cela serait d'utiliser des arguments dynamiques:

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

Ce n'est pas la réponse que vous recherchez, et vous le savez peut-être, mais je pensais que je la donnerais quand même parce que si quelqu'un se posait la question de le faire, non par curiosité mais pour le travail. Ils devraient probablement avoir la «bonne chose à faire».

nichochar
la source
1
J'ai supprimé votre note « PS », nichochar. Nous savons tous à quel point Python est élégant :-) Je ne pense pas que cela fasse partie du corps de la réponse.
Dimitris Fasarakis Hilliard
6
Je pense que vous auriez pu le faire add = sumsi vous alliez dans cette voie
codykochmann
3

Simplement:

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

Si vous êtes prêt à en 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 effectuer les opérations suivantes:

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

Si vous souhaitez add(x)(y)(z)renvoyer facilement le résultat et être plus appelable, le sous-classement intest la solution.

un invité
la source