Nommage forcé des paramètres en Python

111

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 manières suivantes:

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

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

Le problème que nous rencontrons est que certaines de nos plus grandes fonctions se développent, les gens peuvent ajouter des paramètres entre spacinget collapse, ce qui signifie que les mauvaises valeurs peuvent aller vers des paramètres qui ne sont pas nommés. De plus, parfois, on ne sait pas toujours ce qui doit entrer. Nous recherchons un moyen de forcer les gens à nommer certains paramètres - pas seulement un standard de codage, mais idéalement un drapeau ou un plugin pydev?

de sorte que dans les 4 exemples ci-dessus, seul le dernier passerait la vérification car tous les paramètres sont nommés.

Il y a de fortes chances que nous ne l'activerons que pour certaines fonctions, mais toute suggestion sur la façon de l'implémenter - ou si c'est même possible serait appréciée.

Mark Mayo
la source

Réponses:

214

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

À partir de la documentation :

Les paramètres après «*» ou «* identifiant» sont des paramètres de mots-clés uniquement et ne peuvent être transmis que des arguments de mots-clés utilisé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)

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

def foo(pos, *, forcenamed, **kwargs):
Eli Bendersky
la source
32

Vous pouvez forcer les gens à utiliser des arguments de mot-clé 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 positionnel sans nom, vous obligez tous ceux qui appellent la fonction à utiliser les arguments de mot-clé, ce que je pense que vous demandiez. En Python2, le seul moyen de le 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 car vous devrez alors mettre une coche pour n'accepter que l'argument dont vous avez besoin.

Martega
la source
11

Certes, la plupart des langages de programmation font de l'ordre des paramètres une partie du contrat d'appel de fonction, mais cela n'a pas besoin d'être ainsi. 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. En plus d'autres bonnes réponses pour Python 2, veuillez tenir compte des é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 des arguments spacinget de collapsepositionner (sans exception) serait:

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

La convention de ne pas utiliser d'éléments privés appartenant à d'autres modules est déjà très basique en Python. Comme avec Python lui-même, cette convention pour les paramètres ne serait que semi-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 _pet une exception augmentée par la première instruction de la fonction.

Caractéristiques:

  • Les paramètres avant _p=__named_only_startsont admis par position (ou par nom).
  • Les paramètres après _p=__named_only_startdoivent être fournis uniquement par leur nom (à moins que la connaissance de l'objet sentinelle spécial ne __named_only_startsoit obtenue et utilisée).

Avantages:

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

Les inconvénients:

  • La vérification se produit au moment de l'exécution, pas au moment de la compilation.
  • Utilisation d'un paramètre supplémentaire (mais pas d'un argument) et d'une vérification supplémentaire. Petite dégradation des performances par rapport aux fonctions régulières.
  • La fonctionnalité est un hack sans support direct par la langue (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_startdans la bonne position. Oui, cela peut également être considéré comme un pro.

Veuillez garder à l'esprit que cette réponse n'est valable que pour Python 2. Python 3 implémente le mécanisme similaire, mais très élégant, supporté par le langage décrit dans d'autres réponses.

J'ai trouvé que lorsque j'ouvre mon esprit et que j'y pense, aucune question ou décision d'une autre ne semble stupide, stupide ou simplement stupide. Bien au contraire: j'apprends généralement beaucoup.

Mario Rossi
la source
"La vérification se produit pendant l'exécution, pas pendant la compilation." - Je pense que c'est vrai pour toutes les vérifications d'arguments de fonction. Tant que vous n'exécutez pas la ligne de l'appel de fonction, vous ne savez pas toujours quelle fonction est exécutée. En outre, +1 - c'est intelligent.
Eric
@Eric: C'est juste que j'aurais préféré la vérification statique. Mais vous avez raison: cela n'aurait pas du tout été Python. Bien que ce ne soit pas un point décisif, la construction "*" de Python 3 est également vérifiée dynamiquement. Merci pour votre commentaire.
Mario Rossi
De plus, si vous nommez la variable module _named_only_start, il devient impossible de la référencer à partir d'un module externe, ce qui enlève un pro et un con. (les seuls traits de soulignement à la portée du module sont privés, IIRC)
Eric
En ce qui concerne la dénomination de la sentinelle, nous pourrions également avoir à la fois un __named_only_startet un named_only_start(pas de trait de soulignement initial), le second indiquant que le mode nommé est "recommandé", mais pas au niveau d'être "activement promu" (car l'un est public et le autre n'est pas). En ce qui concerne le "caractère privé" de _namescommencer par des traits de soulignement, ce n'est pas très fortement imposé par le langage: il peut être facilement contourné par l'utilisation d'importations spécifiques (non *) ou de noms qualifiés. C'est pourquoi plusieurs documents Python préfèrent utiliser le terme «non public» au lieu de «privé».
Mario Rossi
6

Vous pouvez le faire d'une manière qui fonctionne à la fois dans Python 2 et Python 3 , en créant un premier argument de mot-clé "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 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 modifiez la fonction en:

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

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

Cela vous permettra de positionner des arguments de mots clés supplémentaires n'importe où après _kw, sans vous obliger à les placer après la dernière entrée. Cela a souvent du sens, par exemple, grouper les choses de manière logique ou organiser 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 def(**kwargs)et à la perte des informations de signature dans votre éditeur intelligent. Votre contrat social consiste à fournir certaines informations, en forçant (certaines d'entre elles) à exiger des mots-clés, l'ordre dans lequel ils sont présentés est devenu inutile.

Anthon
la source
2

Mettre à jour:

J'ai réalisé que l'utilisation **kwargsne résoudrait pas le problème. Si vos programmeurs changent les arguments de fonction comme ils le souhaitent, on pourrait, par exemple, changer la fonction en ceci:

def info(foo, **kwargs):

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

Cela revient vraiment à ce que dit Bryan.


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

En général, lors du changement de fonction, les nouveaux arguments doivent toujours aller à la fin. Sinon, cela casse le code. Cela devrait être évident.
Si quelqu'un modifie la fonction pour que le code se brise, ce changement doit être rejeté.
(Comme le dit Bryan, c'est comme un contrat)

(...) Parfois, ce qui doit entrer n'est pas toujours clair.

En regardant la signature de la fonction (ie def info(object, spacing=10, collapse=1)) on devrait immédiatement voir que tout argument qui n'a pas de valeur par défaut, est obligatoire.
Ce que l'argument est pour, devrait entrer dans le docstring.


Ancienne réponse (conservée par souci d'exhaustivité) :

Ce n'est probablement pas une bonne solution:

Vous pouvez définir les fonctions de cette manière:

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')

kwargsest un dictionnaire contenant n'importe quel argument de mot-clé. Vous pouvez vérifier si un argument obligatoire est présent et sinon, lever une exception.

L'inconvénient est que ce n'est peut-être plus aussi évident, quels arguments sont possibles, mais avec une docstring appropriée, ça devrait aller.

Félix Kling
la source
3
J'ai mieux aimé votre ancienne réponse. Mettez simplement un commentaire expliquant pourquoi vous n'acceptez que ** kwargs dans la fonction. Après tout, n'importe qui peut changer n'importe quoi dans le code source - vous avez besoin d'une documentation pour décrire l'intention et le but de vos décisions.
Brandon
Il n'y a pas de réponse réelle dans cette réponse!
Phil
2

Les arguments de mots clés python3 uniquement ( *) 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'

Cela peut être simulé en utilisant ce qui suit, je note ai pris la liberté de passer TypeErrorà KeyErrordans le cas, il ne serait pas « requis argument nommé » trop de travail à faire que le même type d'exception aussi bien

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 doit être évitée si vous êtes uniquement python3.x

Anthony Sottile
la source
Ah, kwargs.pop('foo')est donc un idiome Python 2? J'ai besoin de mettre à jour mon style de codage. J'utilisais toujours cette approche en Python 3 🤔
Neil
0

Vous pouvez déclarer vos fonctions comme réception **argsuniquement. Cela nécessiterait 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.
Noufal Ibrahim
la source
1
Non seulement vous devez ajouter des vérifications de mots clés, mais pensez à un consommateur qui sait qu'il doit appeler une méthode avec la signature foo(**kwargs). Qu'est-ce que je passe là-dedans? foo(killme=True, when="rightnowplease")
Dagrooms
Cela dépend vraiment. Considérez dict.
Noufal Ibrahim
0

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

Si vous souhaitez toujours le faire, utilisez un décorateur de fonctions 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_argsest laissée comme un exercice pour le lecteur.

Je ne me dérangerais pas. Il sera lent à chaque fois que la fonction est appelée, et vous obtiendrez de meilleurs résultats en écrivant le code plus soigneusement.

Daniel Newby
la source
-1

Vous pouvez utiliser le ** opérateur:

def info(**kwargs):

de cette façon, les gens sont obligés d'utiliser des paramètres nommés.

Olivier Verdier
la source
2
Et vous ne savez pas comment appeler votre méthode sans lire votre code, ce qui augmente la charge cognitive de votre consommateur :(
Dagrooms
Pour la raison mentionnée, c'est une très mauvaise pratique et devrait être évitée.
David S.
-1
def cheeseshop(kind, *arguments, **keywords):

en python si vous utilisez * args cela signifie que vous pouvez passer n-nombre d'arguments pour ce paramètre - qui sera une liste à l'intérieur de la fonction pour accéder

et si vous utilisez ** kw cela signifie ses arguments de mot-clé, qui peuvent être accédés en tant que dict - vous pouvez passer n-nombre d'arguments kw, et si vous voulez restreindre cet utilisateur doit entrer la séquence et les arguments dans l'ordre, alors n'utilisez pas * et ** - (sa manière pythonique de fournir des solutions génériques pour les grandes architectures ...)

si vous souhaitez 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
shahjapan
la source
que se passe-t-il si l'espacement est de 0? (réponse, vous obtenez 10). Cette réponse est aussi fausse que toutes les autres réponses ** kwargs pour toutes les mêmes raisons.
Phil
-2

Je ne comprends pas pourquoi un programmeur ajoutera un paramètre entre deux autres en premier lieu.

Si vous souhaitez que les paramètres de fonction soient utilisés avec des noms (par ex. info(spacing=15, object=odbchelper) ), l'ordre dans lequel ils sont définis ne devrait pas avoir d'importance, vous pouvez donc aussi bien mettre les nouveaux paramètres à la fin.

Si vous voulez que la commande compte, vous ne pouvez pas vous attendre à ce que quelque chose fonctionne si vous le modifiez!

Umang
la source
2
Cela ne répond pas à la question. Que ce soit une bonne idée ou non n'a pas d'importance - quelqu'un pourrait le faire de toute façon.
Graeme Perrow
1
Comme Graeme l'a mentionné, quelqu'un le fera de toute façon. De plus, si vous écrivez une bibliothèque pour être utilisée par d'autres, forcer (python 3 uniquement) le passage d'arguments de mot-clé uniquement permet une flexibilité supplémentaire lorsque vous devez refactoriser votre API.
s0undt3ch