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. Doncadd(1)
devrait revenir1
,add(1)(2)
doit retourner1+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?
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
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.
-
type
peut être utilisé pour construire de nouveaux types:type(name, bases, dict) -> a new type
. Pourname
, Nous fournissons une chaîne vide, car le nom n'est pas vraiment nécessaire dans ce cas. Pourbases
(tuple) nous fournissons un(int,)
, qui est identique à l'héritageint
.dict
sont les attributs de classe, où nous attachons le__call__
lambda. -
self.__class__(self + v)
est identique àreturn CustomInt(self + v)
- le nouveau type est construit et renvoyé dans le lambda externe.
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
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".
Simplement:
class add(int):
def __call__(self, n):
return add(self + n)