web-dev-qa-db-fra.com

Nommage forcé des paramètres en Python

En Python, vous pouvez avoir une définition de fonction:

def info(object, spacing=10, collapse=1)

qui peut être appelé de l’une des façons suivantes:

info(odbchelper)                    
info(odbchelper, 12)                
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

grâce à Python permettant des arguments n'importe quel ordre, tant qu'ils sont nommés. 

Le problème que nous rencontrons est que, à mesure que certaines de nos fonctions plus importantes se développent, il est possible que des personnes ajoutent des paramètres entre spacing et collapse, ce qui signifie que les valeurs erronées peuvent aller à des paramètres non nommés. En outre, il est parfois difficile de déterminer clairement ce qui doit être intégré. Nous cherchons un moyen de forcer les gens à nommer certains paramètres - pas seulement une norme de codage, mais idéalement un plugin flag ou pydev?

de sorte que dans les 4 exemples ci-dessus, seul le dernier passe le contrôle car tous les paramètres sont nommés.

Les chances sont que nous ne l'activerons que pour certaines fonctions, mais toute suggestion sur la façon de mettre en œuvre ceci - ou même si cela est possible serait appréciée.

64
Mark Mayo

Dans Python 3 - Oui, vous pouvez spécifier * dans la liste des arguments.

De docs :

Les paramètres après «*» ou «* identificateur» sont des paramètres composés uniquement de mots clés et seuls les arguments de mot-clé utilisés peuvent être passés.

>>> def foo(pos, *, forcenamed):
...   print(pos, forcenamed)
... 
>>> foo(pos=10, forcenamed=20)
10 20
>>> foo(10, forcenamed=20)
10 20
>>> foo(10, 20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes exactly 1 positional argument (2 given)

Ceci peut également être combiné avec **kwargs:

def foo(pos, *, forcenamed, **kwargs):
138
Eli Bendersky

Vous pouvez obliger les utilisateurs à utiliser des arguments de mots clés dans Python3 en définissant une fonction de la manière suivante.

def foo(*, arg0="default0", arg1="default1", arg2="default2"):
    pass

En faisant du premier argument un argument de position sans nom, vous obligez tous ceux qui appellent la fonction à utiliser les arguments de mot-clé, ce que je pense que vous avez demandé. En Python2, la seule façon de faire est de définir une fonction comme celle-ci.

def foo(**kwargs):
    pass

Cela forcera l’appelant à utiliser kwargs, mais ce n’est pas une très bonne solution, vous devrez alors cocher pour ne retenir que l’argument dont vous avez besoin.

23
martega

Certes, la plupart des langages de programmation font de la commande de paramètre une partie du contrat d’appel de fonction, mais cela n’a pas pas besoin de le devenir Pourquoi le ferait-il? Ma compréhension de la question est donc de savoir si Python est différent des autres langages de programmation à cet égard. Outre d’autres bonnes réponses pour Python 2, veuillez prendre en compte les éléments suivants:

__named_only_start = object()

def info(param1,param2,param3,_p=__named_only_start,spacing=10,collapse=1):
    if _p is not __named_only_start:
        raise TypeError("info() takes at most 3 positional arguments")
    return str(param1+param2+param3) +"-"+ str(spacing) +"-"+ str(collapse)

La seule façon pour un appelant de fournir les arguments spacing et collapse en fonction de sa position (sans exception) serait:

info(arg1, arg2, arg3, module.__named_only_start, 11, 2)

La convention de ne pas utiliser déjà des éléments privés appartenant à d'autres modules est très basique en Python. Comme avec Python lui-même, cette convention pour les paramètres ne serait que partiellement appliquée.

Sinon, les appels devront être de la forme:

info(arg1, arg2, arg3, spacing=11, collapse=2)

Un appel

info(arg1, arg2, arg3, 11, 2)

attribuerait la valeur 11 au paramètre _p et une exception générée par la première instruction de la fonction.

Les caractéristiques:

  • Les paramètres précédant _p=__named_only_start sont admis positionnellement (ou par nom).
  • Les paramètres après _p=__named_only_start doivent être fournis uniquement par leur nom (à moins que des informations sur l'objet sentinelle spécial __named_only_start ne soient obtenues et utilisées).

Avantages:

  • Les paramètres sont explicites en nombre et en signification (le dernier si les bons noms sont également choisis, bien sûr).
  • Si la sentinelle est spécifiée en tant que premier paramètre, tous les arguments doivent être spécifiés par nom.
  • Lors de l'appel de la fonction, il est possible de passer en mode positionnel en utilisant l'objet sentinelle __named_only_start dans la position correspondante.
  • Une meilleure performance que d'autres alternatives peut être anticipée.

Les inconvénients:

  • La vérification a lieu à l'exécution, pas à la compilation.
  • Utilisation d'un paramètre supplémentaire (sans argument) et d'une vérification supplémentaire. Légère dégradation des performances par rapport aux fonctions habituelles.
  • La fonctionnalité est un hack sans support direct du langage (voir note ci-dessous).
  • Lors de l'appel de la fonction, il est possible de passer en mode positionnel en utilisant l'objet sentinelle __named_only_start dans la bonne position. Oui, cela peut aussi être vu comme un pro.

N'oubliez pas que cette réponse n'est valable que pour Python 2. Python 3 implémente le mécanisme similaire, mais très élégant, prenant en charge le langage, décrit dans d'autres réponses.

J'ai découvert que, lorsque j'ouvre l'esprit et que j'y réfléchis, aucune question ou décision d'un autre ne semble stupide, stupide ou tout simplement idiote. Bien au contraire: j'apprends généralement beaucoup.

9
Mario Rossi

Vous pouvez faire cela d'une manière qui fonctionne à la fois dans Python 2 et dans Python 3 , en faisant un premier argument "faux" avec une valeur par défaut qui ne se produira pas "naturellement". Cet argument de mot clé peut être précédé d'un ou de plusieurs arguments sans valeur:

_dummy = object()

def info(object, _kw=_dummy, spacing=10, collapse=1):
    if _kw is not _dummy:
        raise TypeError("info() takes 1 positional argument but at least 2 were given")

Cela permettra:

info(odbchelper)        
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

mais non:

info(odbchelper, 12)                

Si vous changez la fonction pour:

def info(_kw=_dummy, spacing=10, collapse=1):

alors tous les arguments doivent avoir des mots-clés et info(odbchelper) ne fonctionnera plus.

Cela vous permettra de positionner des arguments de mot clé supplémentaires n'importe où après _kw, sans vous obliger à les mettre après la dernière entrée. Cela a souvent du sens, par exemple grouper les choses de manière logique ou classer les mots-clés par ordre alphabétique peut aider à la maintenance et au développement.

Il n’est donc pas nécessaire de revenir à l’utilisation de def(**kwargs) et de perdre les informations de signature dans votre éditeur intelligent. Votre contrat social consiste à fournir certaines informations, en obligeant (certaines d’entre elles) à exiger des mots-clés, leur ordre de présentation est devenu inutile.

7
Anthon

Mise à jour:

J'ai réalisé que l'utilisation de **kwargs ne résoudrait pas le problème. Si vos programmeurs changent les arguments de la fonction comme ils le souhaitent, vous pouvez, par exemple, changer la fonction comme suit:

def info(foo, **kwargs):

et l'ancien code s'effondrerait à nouveau (car chaque appel de fonction doit maintenant inclure le premier argument).

Cela revient vraiment à ce que dit Bryan.


(...) des personnes peuvent ajouter des paramètres entre spacing et collapse (...)

En général, lors du changement de fonction, les nouveaux arguments doivent toujours aller jusqu'au bout. Sinon, le code est cassé. Devrait être évident.
Si quelqu'un modifie la fonction pour que le code soit cassé, cette modification doit être rejetée.
(Comme le dit Bryan, c'est comme un contrat)

(...) Parfois, il n’est pas toujours clair de ce qui doit être fait.

En regardant la signature de la fonction (c'est-à-dire def info(object, spacing=10, collapse=1)), il faut immédiatement voir que tout argument comportant non une valeur par défaut est obligatoire.
Qu'est-ce que l'argument est pour, devrait aller dans la docstring.


Ancienne réponse (conservée pour compléter) :

Ce n'est probablement pas une bonne solution:

Vous pouvez définir les fonctions de cette façon:

def info(**kwargs):
    ''' Some docstring here describing possible and mandatory arguments. '''
    spacing = kwargs.get('spacing', 15)
    obj = kwargs.get('object', None)
    if not obj:
       raise ValueError('object is needed')

kwargs est un dictionnaire contenant n'importe quel mot clé. Vous pouvez vérifier si un argument obligatoire est présent et sinon, déclencher une exception.

L'inconvénient est que les arguments possibles ne sont peut-être plus aussi évidents, mais avec une docstring appropriée, tout devrait bien se passer.

2
Felix Kling

Vous pouvez déclarer que vos fonctions reçoivent uniquement **args. Cela imposerait des arguments de mots clés, mais vous auriez du travail supplémentaire pour vous assurer que seuls les noms valides sont transmis. 

def foo(**args):
   print args

foo(1,2) # Raises TypeError: foo() takes exactly 0 arguments (2 given)
foo(hello = 1, goodbye = 2) # Works fine.
1
Noufal Ibrahim

Les arguments python3 contenant uniquement des mots clés (*) peuvent être simulés dans python2.x avec **kwargs.

Considérez le code python3 suivant:

def f(pos_arg, *, no_default, has_default='default'):
    print(pos_arg, no_default, has_default)

et son comportement:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 3 were given
>>> f(1, no_default='hi')
1 hi default
>>> f(1, no_default='hi', has_default='hello')
1 hi hello
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required keyword-only argument: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() got an unexpected keyword argument 'wat'

Ceci peut être simulé en utilisant ce qui suit, note J'ai pris la liberté de passer de TypeError à KeyError dans le cas "argument nommé nommé", il ne serait pas trop difficile de créer le même type d'exception

def f(pos_arg, **kwargs):
    no_default = kwargs.pop('no_default')
    has_default = kwargs.pop('has_default', 'default')
    if kwargs:
        raise TypeError('unexpected keyword argument(s) {}'.format(', '.join(sorted(kwargs))))

    print(pos_arg, no_default, has_default)

Et comportement:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes exactly 1 argument (3 given)
>>> f(1, no_default='hi')
(1, 'hi', 'default')
>>> f(1, no_default='hi', has_default='hello')
(1, 'hi', 'hello')
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
KeyError: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in f
TypeError: unexpected keyword argument(s) wat

La recette fonctionne aussi bien dans python3.x, mais devrait être évitée si vous êtes python3.x uniquement

1
Anthony Sottile

Vous pouvez utiliser l'opérateur **:

def info(**kwargs):

ainsi, les utilisateurs sont obligés d'utiliser des paramètres nommés.

0
Olivier Verdier

Comme d'autres réponses le disent, changer les signatures des fonctions est une mauvaise idée. Ajoutez de nouveaux paramètres à la fin ou corrigez tous les appelants si des arguments sont insérés.

Si vous voulez toujours le faire, utilisez un function decorator et la fonction inspect.getargspec . Il serait utilisé quelque chose comme ceci:

@require_named_args
def info(object, spacing=10, collapse=1):
    ....

La mise en œuvre de require_named_args est laissée comme un exercice pour le lecteur.

Je ne me dérangerais pas. Chaque fois que la fonction est appelée, le processus sera lent et vous obtiendrez de meilleurs résultats si vous écrivez du code plus attentivement.

0
Daniel Newby
def cheeseshop(kind, *arguments, **keywords):

en python si use * args, cela signifie que vous pouvez passer n nombre d’arguments pour ce paramètre - ce qui sera une liste dans la fonction à laquelle accéder

et si use ** kw, cela signifie que ses arguments de mots-clés peuvent être utilisés comme dicte - vous pouvez passer n nombre d'arguments kw, et si vous souhaitez restreindre cet utilisateur, vous devez entrer la séquence et les arguments dans l'ordre puis ne pas utiliser * et ** - (sa façon pythonique de fournir des solutions génériques pour les grandes architectures ...)

si vous voulez restreindre votre fonction avec des valeurs par défaut, vous pouvez vérifier à l'intérieur

def info(object, spacing, collapse)
  spacing = spacing or 10
  collapse = collapse or 1
0
shahjapan