Passer des arguments sous forme d'un dictionnaire ou de paramètres nommés ?

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je travaille avec Python3.5 et j’ai une fonction qui fait deux appels à une API :

1
2
3
4
5
class Simulation:
    def step(self, steps, parameters=None):
        if parameters is not None:
            requests.post(self.endpoint("configure"), json=parameters)
        requests.post(self.endpoint("step"), json={"steps": steps})

Le nombre de paramètres est variable :

1
2
3
4
sim = Simulation()
sim.step(1, {"param1": 1})
sim.step(1, {"param2": 1})
sim.step(1, {"param1": 2, "param3": 2})

La question que je me pose est si l’interface suivante, avec des paramètres nommés plutôt qu’un dictionnaire, est préférable (plus pratique, plus idiomatique, etc.) :

1
2
3
4
5
6
7
8
9
class Simulation:
    def step(self, steps, **kwargs):
        requests.post(self.endpoint("configure"), json=kwargs)
        requests.post(self.endpoint("step"), json={"steps": steps})

sim = Simulation()
sim.step(1, param1=1)
sim.step(1, param2=1)
sim.step(1, param1=2, param3=2)

Merci.

+0 -1

Salut,

Déjà, on sait qu’avec les opérateurs splat les deux versions sont plus ou moins équivalentes, il n’y en a pas une qui offre plus de possibilités que l’autre. Dans le cas où les clefs sont toutes des chaînes de caractères, bien sûr.

Partant de là, j’opterais plutôt pour la seconde solution, plus concise et plus claire à mes yeux. Le problème est si tu as des arguments dont le nom n’est pas un identifiant Python valide (là ça deviendrait un peu bizarre), ou si tu as des conflits avec les noms d’autres paramètres (steps).

Au passage, vu que l’appel à configure ne semble pas indispensable, autant ne pas le faire (dans un cas comme dans l’autre) si le dictionnaire des paramètres est vide.

Salut ! J’aime bien ta première version car tout est clair, tu passes un dico à step directement repris dans la fonction requests.post.

1
2
3
4
5
class Simulation:
    def step(self, steps, parameters=None):
        if parameters:
            requests.post(self.endpoint("configure"), json=parameters)
        requests.post(self.endpoint("step"), json={"steps": steps})

Je ne sais pas combien d’arguments prends ton appel vers configure mais intuitivement on peut imaginer l’appel à la fonction step de deux manières:

Avec ta première méthode.
1
2
3
4
5
6
options = {
    'param': 1,
    'param2': 2,
    'param3': 3
}
simul.step('steps12', options)
Avec ta seconde méthodes
1
2
3
4
param = 1
param2 = 2
param3 = 3
simul.step('steps12', param=param, param2=param2, param3=param3) #voir plus...

Petit exemple qui peut répondre à ta question (pris dans le code du module requests), je t’ai sélectionné une ligne.

@entwanne : la première me semble plus sécurisante. Je n’ai pas l’impression qu’il vaille le coup de s’exposer aux limitations que tu mentionnes pour économiser les caractères dict():

1
sim.step(1, dict(param1=2, param3=2))
+0 -0
Avec ta seconde méthodes
1
2
3
4
param = 1
param2 = 2
param3 = 3
simul.step('steps12', param=param, param2=param2, param3=param3) #voir plus...
grolip

Pas vraiment un argument :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
options = {
    'param': 1,
    'param2': 2,
    'param3': 3,
}
simul.step('steps12', **options)

# et si les options sont paramétrées juste avant l'appel de la fonction :

simul.step('steps12', **{
    'param': 1,
    'param2': 2,
    'param3': 3
})

Autrement dit, ça ne change pas grand chose niveau syntaxe. Personnellement, j’aurai tendance à passer un dict si ce sont de réels données qui sont passés. Si ce sont des paramètres, je tendrai plutôt vers la version foo(**kwargs) qui facilite l’appel quand on veut juste modifier quelques paramètres.

Pourquoi pas un mixte des deux alors ?

1
2
3
4
5
6
7
def step(self, steps, parameters={}, **kwargs):
    params = {}
    params.update(parameters)
    params.update(kwargs)
    if params:
        requests.post(self.endpoint("configure"), json=params)
    requests.post(self.endpoint("step"), json={"step": steps})

Comme ça plus de problème, sauf peut-être l’ordre des updates… :D

Outre le fait que mixer les deux n’apporte rien à part de la confusion, j’attire l’attention des potentiels lecteurs sur le fait qu’il est très souvent une mauvaise idée d’assigner un mutable comme valeur par défaut à un argument d’une fonction. Cet argument n’est évalué qu’une fois et est donc partagé par tous les appels de la fonction utilisant ce défaut, ce qui peut conduire à des effets de bords non désirés si on le modifie dans le corps de la fonction :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def add_beer(bar={}):
    if 'beer' not in bar:
        bar['beer'] = 1
    else:
        bar['beer'] += 1
    return bar

print(add_beer())  # {'beer': 1}
print(add_beer())  # {'beer': 2}
print(add_beer() is add_beer())  # True (et on a 4 bières!)
+0 -0

Dans l’exemple le paramètre par défaut n’est jamais modifié, le fait que ce soit un dictionnaire vide sert à gagner du temps et de la clarté.

Je n’ai rien inventé, la bibliothèque tkinter offre aussi ce choix de paramètre à l’utilisateur, sans confusion me semble-t-il.

Aussi je ne pense pas que ce soit une mauvaise idée de définir un mutable en paramètre par défaut, juste qu’il faut savoir de quoi il en retourne. La plupart du temps, avec un mutable vide, ça sert à clarifier le type de donnée attendu, et dans de rare cas, l’effet de bord dont tu parles peut être désirable.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte