Forum Programmation.python Argument de fonction récurrent

Posté par (page perso) .
Tags : aucun
0
31
mai
2012

Bonjour âme généreuse,

Je souhaite passer un argument à des fonctions de manière transparente et automatique car cet argument est quasi nécessaire pour la plupart des fonctions.

Je souhaiterais pouvoir écrire un truc comme ça :

def function1(arg1, arg2, arg_reccurent):
    print(arg_recurrent)
    # …
def function2(arg1, arg2, arg_reccurent):
    function1(arg2, arg1)
arg_r = 42
function2(1, 2, arg_r)
--> 42

Le but est de passer en paramètre une variable qui serait globale autrement. Alors j'entends des voix dans le fond de la salle, notamment le mot « singleton », mais c'est une fausse bonne idée et n'est qu'une classe unique cachant une variable à portée globale.

Quelqu’un aurait une idée ?

  • # Functools ?

    Posté par (page perso) . Évalué à 4.

    Le module functools de la lib standard permet, comme son nom l'indique, de faire de la programmation orientée fonctionnelle ; regarde du coté de partial, peut-être que ça t'ira :

    from functools import partial
    
    def foo(a, b):
        print a, b
    
    bar = partial(foo, a=1)
    
    bar(b=5) #> 1, 5
    
    
    • [^] # Re: Functools ?

      Posté par (page perso) . Évalué à 1.

      Mettons, que je définie des fonctions et leurs formes « raccourcies » avec partial. C'est pas mal, en déclarant comme globales le nom des fonctions donc j'ai besoin au seins d'autres fonctions, je peux m'en servir partout.

      Néanmoins, après plusieurs lignes de codes, je décide de changer la variable par défaut, a=1 dans ton exemple. Comment je fais pour répercuter ça partout ?

      • [^] # Re: Functools ?

        Posté par (page perso) . Évalué à 1.

        Il semblerait que ça ait avoir avec functools.update_wrapper, mais je ne comprends pas trop comment ça marche…

      • [^] # Re: Functools ?

        Posté par (page perso) . Évalué à 3.

        Sinon utilise une classe avec un attribut.

        • [^] # Re: Functools ?

          Posté par . Évalué à 3.

          Ça me semble la solution la plus simple, et qui évite de se reposer sur des bidouilles non maitrisées. Et extensible, vu que les fonctionnalités demandées ont l'air d'évoluer.

      • [^] # Re: Functools ?

        Posté par (page perso) . Évalué à 1.

        Le problème avec a=1 c'est que '1' est un int donc 'immutable'. Si tu passais une liste par exemple, tu pourrais la modifier à posteriori, même si je ne suis pas sûr que ce soit une bonne idée de structurer ton code comme ça.
        Comme suggéré juste au dessus, tu devrais peut être envisager une approche plus objet. Je tape du python toute la journée et je n'utilise jamais 'global' ; je ne dis pas que c'est inutile, mais si on en vient à en utiliser beaucoup c'est qu'il y a sûrement un problème de design.

  • # wrapper et decorateur

    Posté par (page perso) . Évalué à 1. Dernière modification le 01/06/12 à 14:44.

    Faut probablement l'améliorer et l'adapter à tes besoins mais ça marche.

    Le code devrait parler de lui même:

    #!/usr/bin/env python
    
    def _function1(name, arg_recurrent):
            """ this function just print there args.
            We want to make it callable with only one arg"""
            print name, arg_recurrent
    
    
    def defaultArg(function, **defaultArgs):
            """ wrap a function to automatically add defaultArgs to the call"""
            def newFunction(*args, **kwords):
                    for k, item in defaultArgs.items():
                            kwords.setdefault(k, item)
                    return function(*args, **kwords)
            return newFunction
    
    def canCall(functionToWrap):
            """ Automatically replace functionToWrap by wrap of functionToWrap when calling the function"""
            def decorator(function):
                    def newFunction(*args, **kwords):
                            newfunctionToWrap = defaultArg(functionToWrap, arg_recurrent=kwords.get("arg_recurrent",args[-1]))
                            function.func_globals[functionToWrap.func_name] = newfunctionToWrap
                            function(*args, **kwords)
                            function.func_globals[functionToWrap.func_name] = functionToWrap
                    return newFunction
            return decorator
    
    # create a new function
    function1 = defaultArg(_function1, arg_recurrent=3.14)
    
    def function2(arg_recurrent):
            """ create the fonction in the local namespace"""
            function1 = defaultArg(_function1, arg_recurrent=arg_recurrent)
            function1("function2")
    
    def function3():
            """ use the global function"""
            function1("function3")
    
    @canCall(_function1)
    def function4(arg_recurrent):
            # here we call the wrapped version of _function1
            _function1("function4")
    
    
    @canCall(_function1)
    def function5(arg_recurrent):
            print arg_recurrent,
            arg_recurrent += 10
            print arg_recurrent,
            _function1("function5")
    
    arg_r = 42
    function2(arg_r)
    function3()
    function4(arg_r+1)
    function4(arg_r-1)
    function1("main")
    _function1("_function1", 52)
    function5(arg_r)
    
    class Int:
            def __init__(self, value):
                    self.value = value
    
            def __iadd__(self, value):
                    self.value += value
    
            def __str__(self):
                    return str(self.value)
    
    function5(Int(arg_r))
    
    

    Matthieu Gautier|irc:starmad

    • [^] # Re: wrapper et decorateur

      Posté par (page perso) . Évalué à 1.

      Ça me paraît plutôt bien ! En tout cas, la voie est tracée, merci bien !

    • [^] # Re: wrapper et decorateur

      Posté par . Évalué à 7.

      Tant de code difficile à comprendre et maintenir pour réinventer la roue que sont les classes et les membres… Je ne parle même pas de l'utilisation de noms communs comme "Int" qui font un conflit mental avec le type de base "int".

      • [^] # Re: wrapper et decorateur

        Posté par . Évalué à 5.

        Ha, je me demandais si j'étais le seul à trouver ça illisible…

        • [^] # Re: wrapper et decorateur

          Posté par (page perso) . Évalué à 0.

          Ha ben quand on veut pas faire des trucs standards faut pas s'étonner que la solution soit pas super propre. Et puis il y a seulement deux fonctions utiles dans mon code, le reste c'est des exemples.

          Après, pour avoir une variable commune à plusieurs fonctions mais pas à toutes, LA solution python (et pas que) c'est l'objet.
          Mais c'est pas vraiment ce qui est demandé. Ce que veut Jiehong c'est rajouter des closures à des fonctions. Ça doit être possible mais ça demande de descendre très bas au niveau des objets python, accéder au code des fonctions en dynamique et recréer un objet code en y rajoutant les closures. Mais ça ne se fait pas en deux heures.

          Le fait d'utiliser des objets oblige à mettre du self partout. Ça répond pas au problème qui est d'éviter de passer un argument à une fonction.

          Matthieu Gautier|irc:starmad

          • [^] # Re: wrapper et decorateur

            Posté par . Évalué à 6. Dernière modification le 01/06/12 à 19:03.

            Plutôt que de tenter à tout prix de faire ce qu'on a en tête, il vaut mieux considérer d'abord si c'est une bonne chose. Si par exemple il faut faire des contorsions dans le code, des bidouilles difficiles à lire, des choses qu'on aurait du mal à expliquer, alors c'est un bon indice que c'est probablement une mauvaise idée.
            Pour ce qui est des fermetures, elles existent en Python, il s'agit de lambda. Il souhaite également pouvoir les modifier après, il souhaite une syntaxe pour les faire sans objets, etc.
            Ça va l'amuser pendant quelques minutes de chercher une solution à ce problème, et il va s'extasier devant le fait qu'on peut cumuler décorateurs, modifier les paramètres. Mais ça sera mauvais sur le long terme, on aura rajouté des couches pour faire quelque chose que le langage permettait déjà de faire (les classes), parce qu'on aura estimé qu'on était plus intelligent que le langage, ce qui est faux dans la totalité des cas.
            Ce code est difficile à maintenir. Et il est lourd à utiliser : il faut utiliser des décorateurs, utiliser des types personnalisés comme Int. On n'est même plus dans la complexité, on est dans la complication.

            • [^] # Re: wrapper et decorateur

              Posté par (page perso) . Évalué à 2.

              Moi, je répond juste à la question. Je dit pas que c'est ce qu'il faut faire (vous le faites suffisamment bien, pas besoin que j'en rajoute une couche).

              Par contre, j'ai trouvé que c'était un bon exercice, j'y ai réfléchi et j'ai trouvé une solution, je la partage.

              Niveau complexité, certes c'est plus compliqué qu'une variable globale ou une classe, mais le code important se résume à une dizaine de lignes, c'est pas ce qui est du plus compliqué.

              Après je te rassure (si jamais tu pouvais potentiellement t'inquiéter de ce que je puisse faire) l'idée ne me serais même pas venu à l'esprit. Je préfèrerait encore me taper le copier-coller des arguments.

              De plus je ne connais pas le problème réel de Jiehong.

              • La fonction à appeler peut être une fonction d'un module extérieur (donc impossibilité de la modifier)
              • Il y a peut être une dizaine d'arguments identiques, ce qui expliquerait qu'il n'est pas envie de les répéter 36 fois (même si ça peut se simplifier à coup de [] ou *{})

              À ce moment, que tu utilises des classe et des globales, tu te retrouves quand même à faire des wrappers(/lambdas/méthodes).
              Et si tu as 10 fonctions à wrappers que tu utilises dans une 10aine de tes fonctions tu te retrouves à faire des wrappers à tous va (éventuellement un peu partout dans ton code). À ce moment le code est plus simple mais à maintenir c'est le bordel.

              Je répond juste à sa question. C'est lui le dev de son projet, à lui de savoir ce qu'il doit utiliser.

              Matthieu Gautier|irc:starmad

              • [^] # Re: wrapper et decorateur

                Posté par (page perso) . Évalué à 2.

                Bon je revient à la charge avec une solution plus propre qui devrait plaire à pas mal de monde (enfin j'espère) :

                wrapper.py:

                class Wrapper(object):
                    def __init__(self, **kwords):
                        self.kwords = kwords
                        self.functions = {}
                
                    def add_function(self, function):
                        self.functions[function.func_name] = function
                
                    def __getattr__(self, functionName):
                        try:
                            func = self.functions[functionName]
                        except KeyError:
                            raise AttributeError
                
                        def wrapFunction(*args, **kwords):
                            argsNeeded = list(func.func_code.co_varnames)
                            argsDict = {}
                            for i, arg in enumerate(args):
                                argsDict[argsNeeded[i]] = arg
                            for key, val in kwords.items():
                                if key in argsDict:
                                    raise TypeError("%s() got multiple values for keyword argument '%s'"%(functionName, key))
                                argsDict[key] = val
                            for key,val in self.kwords.items():
                                if key not in argsDict and key in argsNeeded:
                                    argsDict[key] = val
                            for arg in argsNeeded[:-len(func.func_defaults or [])]:
                                if arg not in argsDict:
                                    raise TypeError("%s() need argument '%s'"%(functionName,arg))
                            return func(**argsDict)
                        return wrapFunction
                
                

                test.py:

                #!/usr/bin/env python
                
                from wrapper import Wrapper
                
                
                def function1(arg1, arg2, recurrent_arg, default_arg="default"):
                        print arg1, arg2, recurrent_arg, default_arg
                
                def function2(recurrent_arg, another_recurrent_arg):
                        print recurrent_arg, another_recurrent_arg
                
                
                def function3(arg1, arg2, recurrent_arg):
                        wrapper = Wrapper(recurrent_arg=recurrent_arg)
                        wrapper.add_function(function1)
                        wrapper.add_function(function2)
                
                        print "1 =>",
                        function1(arg1,arg2,recurrent_arg)
                
                        print "2 =>",
                        wrapper.function1(arg1,arg2,recurrent_arg)
                
                        print "3 =>",
                        wrapper.function1(arg1,arg2)
                
                        print "4 =>",
                        wrapper.kwords["recurrent_arg"] = 42
                        wrapper.function1(arg1,arg2)
                
                        print "5 =>",
                        wrapper.kwords["default_arg"] = 3.14
                        wrapper.function1(arg1,arg2)
                
                        print "6 =>",
                        wrapper.function1(arg1,arg2,3)
                
                        print "7 =>",
                        wrapper.function1(arg1, default_arg="cool", arg2=42)
                
                        print "8 =>",
                        wrapper.kwords["arg1"]=100
                        wrapper.function1(default_arg="cool", arg2=42)
                
                        print "9 =>",
                        wrapper.function2(another_recurrent_arg="another")
                
                        print "10 =>",
                        wrapper.kwords["another_recurrent_arg"] = "again"
                        wrapper.function2()
                
                
                function3(1, 2, 3)
                
                

                Ça donne :

                1 => 1 2 3 default
                2 => 1 2 3 default
                3 => 1 2 3 default
                4 => 1 2 42 default
                5 => 1 2 42 3.14
                6 => 1 2 3 3.14
                7 => 1 42 42 cool
                8 => 100 42 42 cool
                9 => 42 another
                10 => 42 again
                
                

                Matthieu Gautier|irc:starmad

                • [^] # Re: wrapper et decorateur

                  Posté par . Évalué à 3.

                  Bon je revient à la charge avec une solution plus propre qui devrait plaire à pas mal de monde (enfin j'espère)

                  Mmhh, je pense que tu n'as pas saisi le problème que nous voyons. Je pense que nous n'avons pas les même objectifs : toi, tu veux absolument faire la chose dont Jiehong parle en respectant strictement ses contraintes, quelques soient les circonvolutions à faire en python. Moi et BFG (d'après ce que je comprends) essayons de trouver un compromis qui résolve son problème et soit lisible par un programmeur « normal », réutilisable, et respecte quelques « bonnes pratiques » de programmation.

                  Perso, je continue à trouver ton code très illisible. C'est marrant d'étirer le langage dans tous les sens, mais je n'écrirais jamais ce genre de chose pour quelqu'un d'autre. Après, pour s'amuser, pourquoi pas, mais en tous cas je n'espère jamais tomber sur ce genre de code pour le reprendre/relire.

                  J'espère que tu ne le prends pas mal (j'ai vaguement eu cette impression dans tes commentaires précédents), je pense que c'est juste que nous avons des buts différents. Mais j'aime bien essayer en général de « bien » faire les choses et de montrer des bouts de code qui me semblent plutôt facilement réutilisables, que d'aller chercher à tout prix les possibilités les plus tordues d'un langage.

                  • [^] # Re: wrapper et decorateur

                    Posté par (page perso) . Évalué à 2.

                    J'espère que tu ne le prends pas mal (j'ai vaguement eu cette impression dans tes commentaires précédents)

                    Un peu, mais ça va, je vais mieux maintenant :-)

                    Je suis globalement d'accord avec toi : Il ne faut pas faire des trucs compliqués juste pour l'envie/parce qu'on sait le faire.

                    Il n'empêche que si la POO répond à beaucoup de problématiques, ce n'est pas la solution à tous les problèmes.

                    Ce n'est pas parce que des fonctions utilisent un ensemble communs de paramètres qu'il faut obligatoirement les regrouper dans une même classe.

                    L'exemple bidon que je vois rapidement c'est un système de véhicules (avec les héritages voiture, camion, …) qui se déplace sur plusieurs terrains.

                    Ce n'est pas parce que la majorité des méthodes de Vehicule utilise les mêmes paramètres (relatifs à l'environnement) qu'ils doivent être mis dans la classe Vehicule. (Tu me diras que ça doit être mis dans la classe Terrain et qu'il faut passer un objet Terrain aux méthodes alors je te répondrais que je suis d'accord mais que ce n'est pas dans la classe Vehicule)

                    Ça s'apparente plus ici à une programmation par contexte. C'est pas de la POO (bien que ça l'utilise en interne) c'est autre chose. On pourrait avoir un code de ce genre:

                    context1 = Context(vehicule1, terrain1)
                    context2 = Context(vehicule2, terrain)
                    compare_context(context1, context2)
                    context1.fonction_de_vehicule() # dans le context1
                    context1.fonction_de_vehicule(vitesse_vent=5) # dans le context1 mais avec un vent de 5m/s
                    
                    

                    Python fournit beaucoup de fonctionnalités qui sortent de la POO classique (duck typing, metaclass, decorator, introspection du code, … ). Ce ne sont pas des trucs alambiqués qui dénaturent python. C'est juste que python n'est pas C++ ou java.

                    Ne pas utiliser ces fonctionnalités (cette puissance) juste parce que c'est pas de la POO est stupide (l'utiliser juste parce que c'est disponible est aussi stupide). Il faut voir en fonction du besoin, que je ne connait pas (comme toi).

                    Après je suis désolé, ma classe Wrapper (qui devrait s'appeler Context) n'est pas compliqué. Elle mérite peut être quelques commentaires et fonctions intermédiaires mais la base de l'algo est extrêmement simple : utiliser un dictionnaire de valeur pour remplir les "trous" dans les arguments lors d'un appel de fonction. Ça se résume en 20/30 lignes et c'est générique.
                    Certes ça demande de comprendre ce qu'il se passe et ça change par rapport à la POO. Mais la POO a apporté les mêmes changements par rapport à la programmation procédural. Ce n'était pas une mauvaise chose et ça ne l'a pas remplacé. (je n'ai toutefois pas la prétention d'inventer un nouveau paradigme de programmation)

                    Matthieu Gautier|irc:starmad

            • [^] # Re: wrapper et decorateur

              Posté par (page perso) . Évalué à 1.

              J'admets que ce n'est pas des plus simples, mais mon but étant bien d'éviter tous les self et compagnie. Mes variables globales sont utilisées par nombre de fonctions, mais pas toutes. De plus, je voulais essayer une solution atypique qui me paraissait pas trop mauvaise (apparemment, non).

              Bon, mais c'est un petit projet perso, c'est la deuxième fois que je ré-écris tout même si ça marchait déjà pas mal.

  • # Le global c'est bien pour du global

    Posté par . Évalué à 7. Dernière modification le 01/06/12 à 18:23.

    Le but est de passer en paramètre une variable qui serait globale autrement. Alors j'entends des voix dans le fond de la salle, notamment le mot « singleton », mais c'est une fausse bonne idée et n'est qu'une classe unique cachant une variable à portée globale.

    Et c'est quoi ton problème avec les variables globales ? Si c'est un paramètre qui doit être utilisé partout, une variable globale est tout à fait indiquée. Si c'est un paramètre spécifique à une « chose » particulière, qui sera réutilisée pour cette « chose », et bien… fait de l'objet ! Ça sert à ça…

        class Recurrent(object):
            def __init__(self, arg_r):
                self.arg_r = arg_r
            def function1(self, arg1, arg2):
                print(self.arg_r)
                # …
            def function2(self, arg1, arg2):
                self.function1(arg1, arg2)
    
        chose = Recurrent(42)
        chose.function2(1, 2)
    
    

    Et sache que singleton n'est pas une formule magique qui résout tous les problèmes. (j'ai l'impression des fois que les designs patterns ça fait plus de mal que de bien)

    • [^] # Re: Le global c'est bien pour du global

      Posté par . Évalué à 3.

      On aura bien sûr corrigé l'appel à function1 qui devait bien sûr s'écrire self.function1.

      • [^] # Re: Le global c'est bien pour du global

        Posté par (page perso) . Évalué à 3.

        J'espère avoir corrigé correctement.

        « Rappelez-vous toujours que si la Gestapo avait les moyens de vous faire parler, les politiciens ont, eux, les moyens de vous faire taire. » Coluche

        • [^] # Re: Le global c'est bien pour du global

          Posté par . Évalué à 2.

          Oui, merci ;-) Mais en même temps, je viens de me rendre compte que j'ai oublié les self aussi dans les définitions des méthodes… bref, les erreurs habituelles que je fais, et qui apparaissent quand on ne teste pas le code qu'on écrit /o\

    • [^] # Re: Le global c'est bien pour du global

      Posté par . Évalué à 3.

      Les singletons sont des variables globales, il n'y a aucune différence entre les deux.

      • [^] # Re: Le global c'est bien pour du global

        Posté par . Évalué à 2.

        Pas tout à fait. Par exemple, dans les langages déficients comme Java où on ne peut pas mettre de « vraies » variables globales en dehors des classes, on peut quand même en mettre dedans. Et pourtant, l'utilisation d'un singleton est préconisée. J'ai souvenir que ça venait d'une histoire de précaution vis à vis de la concurrence à l'initialisation, mais je peux me tromper.

        • [^] # Re: Le global c'est bien pour du global

          Posté par (page perso) . Évalué à 5.

          Le singleton est surtout préconisé quand tu veux être sûr qu'il n'y aura jamais plus d'une instance de ton objet qui existera sur le système, ce n'est pas pour faire une variable globale.

          « Rappelez-vous toujours que si la Gestapo avait les moyens de vous faire parler, les politiciens ont, eux, les moyens de vous faire taire. » Coluche

  • # Évaluation partielle

    Posté par . Évalué à 3. Dernière modification le 02/06/12 à 18:48.

    Je rejoins les commentaires qui t'incitent plutôt à créer un objet encapsulant tes algorithmes, ce qui te permet d'obtenir le résultat attendu. Si tu tiens à l'évaluation partielle, voici une solution un peu moins compliquée que celle de Matthieu.

    Elle fait 6 lignes:

    def bind(x,f):
        def f_x(*argv, **argd):
            argv_x = [ x ]
            argv_x[1:] = argv
            return f(*argv_x, **argd)
        return f_x
    
    

    Ce qui s'utilise commee ça:

    def add(a,b):
        return a + b
    
    add2 = bind(2, add)
    print "La réponse est", add2(40)
    
    

    Tu peux itérer le processus comme dans

    add40and2 = bind(40, bind(2, add))
    print "La réponse est", add40and2()
    
    

    Tu peux varier la préparation de argv_x pour lier le dernier argument, le second etc. Voilà une variante qui lie des arguments de type dictionnaire.

    def bind_argd(x, f):
        def f_d(*argv, **argd):
            argd_x = dict(argd)
            for k,v in x.iteritems(): argd_x[k] = v
            return f(*argv_x, **argd_x)
        return f_x
    
    

    Pour troller un peu, je précise que je n'aime pas du tout programmer en Python. :)

    • [^] # Re: Évaluation partielle

      Posté par . Évalué à 2.

      Vous auriez mieux fait de lire le premier commentaire.

      • [^] # Re: Évaluation partielle

        Posté par . Évalué à 2.

        Néanmoins, après plusieurs lignes de codes, je décide de changer la variable par défaut, a=1 dans ton exemple. Comment je fais pour répercuter ça partout ?

        Il ne dit pas qu'il n'aime pas trop que le nom des variables apparaissent dans les applications partielles, ici?

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.