Considérez:
def foobar(*, foo, bar): if foo: print('foo', end="") if bar: print('bar', end="") if foo and bar: print('No bueno', end='') # I want this to be impossible if not foo and not bar: print('No bueno', end='') # I want this to be impossible print('') foobar(foo='bar') # I want to pass inspection foobar(bar='foo') # I want to pass inspection foobar(foo='bar', bar='foo') # I want to fail inspection foobar() # I want to fail inspection
Existe-t-il un moyen de configurer une fonction de manière à l'appeler de cette façon uniquement lorsque l'inspection est passée uniquement lorsqu'un seul élément parmi foo ou bar est passé, sans vérifier manuellement l'intérieur de la fonction ?
4 Réponses :
En bref: non, vous ne pouvez pas faire ça.
Le plus proche que vous pouvez obtenir pourrait être l'utilisation d'une assertion:
def foobar(foo=None, bar=None): assert bool(foo) != bool(bar) foobar(foo='bar') # Passes foobar(bar='foo') # Passes foobar(foo='bar', bar='foo') # Raises an AssertionError foobar() # Raises an AssertionError
La combinaison des conversions bool
et du ! =
fera un XOR logique.
Attention cependant aux assertions; ils peuvent être désactivés. C'est bien si votre vérification est requise pendant le développement uniquement.
Vérifiez soigneusement l'égalité booléenne au lieu de est None
. Si un seul paramètre falsey était passé, cela déclencherait quand même une exception.
Syntaxiquement non. Cependant, il est relativement facile de le faire en utilisant un décorateur:
>>> @mutually_exclusive('foo', 'bar') ... def foobar(a,b,c, *, foo=None, bar=None, taz=None): ... print(a,b,c,foo,bar,taz) ... >>> foobar(1,2,3, foo=4, taz=5) 1 2 3 4 None 5 >>> foobar(1,2,3, foo=4, bar=5,taz=6) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 7, in inner TypeError: You must specify exactly one of foo, bar
Utilisé comme:
>>> @mutually_exclusive('foo', 'bar') ... def foobar(*, foo=None, bar=None): ... print(foo, bar) ... >>> foobar(foo=1) 1 None >>> foobar(bar=1) None 1 >>> foobar(bar=1, foo=2) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 7, in inner TypeError: You must specify exactly one of foo, bar >>> foobar() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 7, in inner TypeError: You must specify exactly one of foo, bar
Le décorateur ignore les positions et les arguments de mots-clés non inclus dans la liste donnée:
from functools import wraps def mutually_exclusive(keyword, *keywords): keywords = (keyword,)+keywords def wrapper(func): @wraps(func) def inner(*args, **kwargs): if sum(k in keywords for k in kwargs) != 1: raise TypeError('You must specify exactly one of {}'.format(', '.join(keywords))) return func(*args, **kwargs) return inner return wrapper
Si les arguments peuvent être "facultatifs" (c'est-à-dire que vous pouvez spécifier au plus l'un de ces arguments de mot-clé, mais vous pouvez également les omettre tous) remplacez ! = 1
par ou
in (0,1)
selon vos préférences.
Si vous remplacez 1
avec un nombre k
vous généralisez le décorateur pour accepter exactement (ou tout au plus) k
des arguments spécifiés de l'ensemble que vous avez fourni. 1
/ p>
Cela n'aidera en aucun cas PyCharm. Pour autant que je sache actuellement, il est tout simplement impossible de dire à un IDE ce que vous voulez.
Le décorateur ci-dessus a un petit "bug": il considère foo = None
comme si vous avez passé une valeur pour foo
car elle apparaît dans la liste kwargs
. Habituellement, vous vous attendez à ce que la transmission de la valeur par défaut se comporte de la même manière que si vous ne spécifiez pas du tout l'argument.
Pour corriger cela correctement, il faudrait inspecter func
dans wrapper
pour rechercher les valeurs par défaut et changer k dans les mots-clés
avec quelque chose comme k dans les mots-clés et kwargs [k]! = defaults [k]
.
C'est une manière très pythonique d'atteindre les résultats souhaités. Bien que je suggère ValueError
ou RuntimeError
à la place de TypeError
. Préférence personnelle, je suppose.
@PMende J'ai choisi TypeError
car typiquement les erreurs soulevées lorsque vous fournissez les mauvais arguments lèvent TypeError
(par exemple def f (a): pass
puis < code> f (b = 1) soulève TypeError: f () a obtenu un argument de mot-clé inattendu 'b'
. Je pense donc que TypeError
est plus cohérent dans ce domaine cas puisque nous simulons une signature spéciale pour la fonction ... Mais alors c'est une opinion.
Cela fonctionnerait-il également pour un constructeur d'une classe? Pensez à l'exemple canonique d'une classe de cercle pour laquelle des objets seraient générés en utilisant un point médian et soit (exclusivement) un rayon ou un diamètre ...
Et pour une classe de données python?
@GertVdE Vous pouvez définir manuellement le __init__
et y placer le décorateur (étant donné que vous travaillez avec des arguments mutuellement excusables, il y a de fortes chances que vous souhaitiez une autre logique personnalisée de toute façon). Si vous ne voulez pas écrire votre propre __init__
personnalisé, il est probable que vous deviez utiliser des métaclasses. Vous devriez rechercher un tutoriel sur les métaclasses python3.
La bibliothèque standard utilise une simple vérification à l'exécution pour cela:
def foobar(*, foo=None, bar=None): if (foo is None) == (bar is None): raise ValueError('Exactly one of `foo` and `bar` must be provided')
Vous pouvez légèrement refactoriser et prendre deux paramètres non optionnels qui, ensemble, fournissent une valeur:
def foobar(name, value): if name == 'foo': foo = value elif name == 'bar': bar = value else: raise ValueError()
De cette façon, il est impossible de transmettez deux valeurs foo ou bar. PyCharm vous avertirait également si vous ajoutiez des paramètres supplémentaires.
foo
etbar
sont des paramètres de mot clé uniquement dans cette définition defoobar
, et non des paramètres facultatifs. (J'ai vu beaucoup de gens faire l'erreur inverse, mais c'est la première fois que je vois quelqu'un mélanger les choses dans ce sens.)Oui, je sais qu'ils ne sont pas facultatifs pour le moment. Je me demandais s'il y avait un moyen de faire en sorte que cette façon de passer l'inspection lors de l'appel de
foobar
, vous ne pourriez passer qu'un seul parmi foo ou bar.Faites-les tous deux nommés avec un paramètre à aucun et vérifiez cela?
Bien que cela fonctionnerait pour le faire échouer, il passe toujours l'inspection, et donc quelqu'un ne se rendra pas compte que quelque chose ne va pas avant d'exécuter le code. C'est de là que je viens.
Qu'entendez-vous par «il passe toujours l'inspection»? Quel genre d'inspection voulez-vous attraper? Un outil automatisé spécifique? Revue de code par un programmeur d'un certain niveau d'expérience Python?
Fondamentalement, je veux que pycharm me donne un avertissement avant d'exécuter le code, et je veux que python génère une erreur lors de la tentative d'accès à la fonction.