2
votes

Existe-t-il un moyen de forcer des paramètres de fonction mutuellement exclusifs en python?

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 ?


6 commentaires

foo et bar sont des paramètres de mot clé uniquement dans cette définition de foobar , 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.


4 Réponses :


0
votes

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.


1 commentaires

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.



6
votes

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] .


5 commentaires

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.



0
votes

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


0 commentaires

0
votes

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.


0 commentaires