Syntaxe Python pour "si a ou b ou c mais pas tous"

130

J'ai un script python qui peut recevoir zéro ou trois arguments de ligne de commande. (Soit il s'exécute sur le comportement par défaut, soit il a besoin des trois valeurs spécifiées.)

Quelle est la syntaxe idéale pour quelque chose comme:

if a and (not b or not c) or b and (not a or not c) or c and (not b or not a):

?

Chris Wilson
la source
4
peut-être commencer par quelque chose comme `if len (sys.argv) == 0:
Edgar Aroutiounian
6
@EdgarAroutiounian len(sys.argv)sera toujours au moins égal à 1: il inclut l'exécutable sous la forme argv[0].
RoadieRich
10
Le corps de la question ne correspond pas au titre de la question. Voulez-vous vérifier "si a ou b ou c mais pas tous" ou "si exactement l'un des a, b et c" (comme le fait l'expression que vous avez donnée)?
Doug McClean
2
Que pouvez-vous dire sur a + b + c?
gukoff
6
Attendez, questionnez, cela peut prendre zéro ou trois arguments. ne pourriez-vous pas simplement dire if not (a and b and c)(zéro argument), puis if a and b and c(les trois arguments)?
acolyte

Réponses:

236

Si vous voulez dire une forme minimale, allez-y:

if (not a or not b or not c) and (a or b or c):

Ce qui traduit le titre de votre question.

MISE À JOUR: comme correctement dit par Volatility et Supr, vous pouvez appliquer la loi de De Morgan et obtenir l'équivalent:

if (a or b or c) and not (a and b and c):

Mon conseil est d'utiliser la forme la plus importante pour vous et pour les autres programmeurs. Le premier signifie "il y a quelque chose de faux, mais aussi quelque chose de vrai" , le second "il y a quelque chose de vrai, mais pas tout" . Si je devais optimiser ou faire cela dans le matériel, je choisirais le second, ici il suffit de choisir le plus lisible (en tenant également compte des conditions que vous allez tester et de leurs noms). J'ai choisi le premier.

Stefano Sanfilippo
la source
3
Toutes les bonnes réponses, mais cela gagne pour la concision, avec un grand court-circuit. Merci a tous!
Chris Wilson
38
Je le rendrais encore plus concis et j'irais avecif not (a and b and c) and (a or b or c)
Volatilité
208
Ou même if (a or b or c) and not (a and b and c)pour correspondre parfaitement au titre;)
Supr
3
@HennyH Je crois que la question demande "au moins une condition vraie mais pas toutes", pas "une seule condition vraie".
Volatilité
63
@Suprif any([a,b,c]) and not all([a,b,c])
eternalmatt
238

Que diriez-vous:

conditions = [a, b, c]
if any(conditions) and not all(conditions):
   ...

Autre variante:

if 1 <= sum(map(bool, conditions)) <= 2:
   ...
défuz
la source
2
sum(conditions)peut mal tourner si l'un d'eux revient 2par exemple, ce qui est True.
eumiro
7
Vrai, vous auriez besoin d'un mochesum(map(bool, conditions))
jamylak
5
Notez qu'il ne s'agit pas d'un court-circuit, car toutes les conditions sont pré-évaluées.
georg
14
@PaulScheltema La première forme est plus compréhensible pour tout le monde.
cmh
6
Ce "tout et pas tout" est la meilleure et la plus claire des méthodes booléennes, soyez juste conscient de la distinction importante entre un argument étant présent et un argument étant "véridique"
wim
115

Cette question avait déjà de nombreuses réponses très positives et une réponse acceptée, mais jusqu'à présent, toutes étaient distraites par diverses façons d'exprimer le problème booléen et ont manqué un point crucial:

J'ai un script python qui peut recevoir zéro ou trois arguments de ligne de commande. (Soit il fonctionne avec le comportement par défaut, soit il a besoin des trois valeurs spécifiées)

Cette logique ne doit pas être la responsabilité de votre code en premier lieu , elle doit plutôt être gérée parargparsemodule. Ne vous embêtez pas à écrire une instruction if complexe, préférez plutôt configurer votre analyseur d'arguments comme ceci:

#!/usr/bin/env python
import argparse as ap
parser = ap.ArgumentParser()
parser.add_argument('--foo', nargs=3, default=['x', 'y', 'z'])
args = parser.parse_args()
print(args.foo)

Et oui, cela devrait être une option et non un argument positionnel, car après tout, il est facultatif .


édité: Pour répondre à la préoccupation de LarsH dans les commentaires, voici un exemple de la façon dont vous pourriez l'écrire si vous étiez certain de vouloir l'interface avec 3 ou 0arguments positionnels . Je suis d'avis que l'interface précédente est un meilleur style, car les arguments optionnels devraient être des options , mais voici une approche alternative par souci d'exhaustivité. Notez le kwarg de remplacementusagelors de la création de votre analyseur, carargparsesinon, cela générera automatiquement un message d'utilisation trompeur!

#!/usr/bin/env python
import argparse as ap
parser = ap.ArgumentParser(usage='%(prog)s [-h] [a b c]\n')
parser.add_argument('abc', nargs='*', help='specify 3 or 0 items', default=['x', 'y', 'z'])
args = parser.parse_args()
if len(args.abc) != 3:
  parser.error('expected 3 arguments')
print(args.abc)

Voici quelques exemples d'utilisation:

# default case
wim@wim-zenbook:/tmp$ ./three_or_none.py 
['x', 'y', 'z']

# explicit case
wim@wim-zenbook:/tmp$ ./three_or_none.py 1 2 3
['1', '2', '3']

# example failure mode
wim@wim-zenbook:/tmp$ ./three_or_none.py 1 2 
usage: three_or_none.py [-h] [a b c]
three_or_none.py: error: expected 3 arguments
wim
la source
4
Oui, j'ai ajouté cela intentionnellement. Il serait possible de rendre l'argument positionnel et de faire en sorte qu'exactement 3 ou 0 soient consommés, mais cela ne ferait pas un bon CLI donc je ne l'ai pas recommandé.
wim
8
Numéro séparé. Vous ne croyez pas que c'est une bonne CLI, et vous pouvez argumenter sur ce point, et l'OP peut être convaincu. Mais votre réponse s'écarte suffisamment de la question pour que le changement de spécification doive être mentionné. Vous semblez plier les spécifications pour s'adapter à l'outil disponible, sans mentionner le changement.
LarsH
2
@LarsH OK, j'ai ajouté un exemple qui correspond mieux à l'interface d'origine impliquée dans la question. Maintenant, il plie l'outil pour répondre aux spécifications disponibles ...;)
wim
2
C'est la seule réponse que j'ai votée. +1 pour répondre à la vraie question .
Jonathon Reinhart
1
+1. La forme de la CLI est une question tangentielle importante, pas complètement distincte comme une autre personne l'a dit. J'ai voté pour votre message ainsi que d'autres - le vôtre va à la racine du problème et offre une solution élégante, tandis que d'autres articles répondent à la question littérale. Et les deux types de réponses sont utiles et méritent +1.
Ben Lee
32

J'irais pour:

conds = iter([a, b, c])
if any(conds) and not any(conds):
    # okay...

Je pense que cela devrait court-circuiter assez efficacement

Explication

En créant condsun itérateur, la première utilisation de anycourt-circuitera et laissera l'itérateur pointant vers l'élément suivant si un élément est vrai; sinon, il consommera la liste entière et sera False. Le suivant anyprend les éléments restants dans l'itérable, et s'assure qu'il n'y a pas d'autres valeurs vraies ... S'il y en a, la déclaration entière ne peut pas être vraie, donc il n'y a pas un élément unique (donc des courts-circuits encore). Le dernier anyretournera Falseou épuisera l'itérable et sera True.

Remarque: les contrôles ci-dessus vérifient si une seule condition est définie


Si vous souhaitez vérifier si un ou plusieurs éléments, mais pas tous, sont définis, vous pouvez utiliser:

not all(conds) and any(conds)
Jon Clements
la source
5
Je ne comprends pas. Il se lit comme: si vrai et non vrai. Aide moi à comprendre.
rGil
1
@rGil: cela se lit comme "si certaines pommes sont rouges et d'autres pas" - c'est la même chose que de dire "certaines pommes sont rouges, mais pas toutes".
georg
2
Même avec des explications, je ne peux pas comprendre le comportement ... Avec [a, b, c] = [True, True, False]votre code ne devrait-il pas "s'imprimer" False, alors que la sortie attendue est True?
awesoon
6
C'est assez intelligent, MAIS: j'utiliserais cette approche si vous ne saviez pas combien de conditions vous aviez à l'avance, mais pour une liste fixe et connue de conditions, la perte de lisibilité n'en vaut tout simplement pas la peine.
moelleux
4
Cela ne court-circuite pas. La liste est entièrement construite avant d'être transmise iter. anyet allconsommera paresseusement la liste, c'est vrai, mais la liste était déjà complètement évaluée au moment où vous y arrivez!
icktoofay
22

La phrase anglaise:

"Si a ou b ou c mais pas tous"

Se traduit par cette logique:

(a or b or c) and not (a and b and c)

Le mot "mais" implique généralement une conjonction, en d'autres termes "et". De plus, «tous» se traduit par une conjonction de conditions: cette condition, et cette condition, et une autre condition. Le «non» inverse toute cette conjonction.

Je ne suis pas d'accord que la réponse acceptée. L'auteur a négligé d'appliquer l'interprétation la plus directe à la spécification et a négligé d'appliquer la loi de De Morgan pour simplifier l'expression à moins d'opérateurs:

 not a or not b or not c  ->  not (a and b and c)

tout en affirmant que la réponse est une "forme minimale".

Kaz
la source
En fait, cette forme est minime. C'est la forme minimale de PoS pour l'expression.
Stefano Sanfilippo
10

Cela revient Truesi une et une seule des trois conditions est True. Probablement ce que vous vouliez dans votre exemple de code.

if sum(1 for x in (a,b,c) if x) == 1:
eumiro
la source
Pas aussi joli que la réponse de @defuz
jamylak
10

Qu'en est-il: (condition unique)

if (bool(a) + bool(b) + bool(c) == 1):

Remarquez, si vous autorisez également deux conditions, vous pouvez le faire

if (bool(a) + bool(b) + bool(c) in [1,2]):
Casimir et Hippolyte
la source
1
Pour mémoire, la question demande deux conditions. Au moins un, mais pas tous = 1 de tous ou 2 de tous
Marius Balčytis
IMHO, vous devriez épeler le deuxième comme 1 <= bool(a) + bool(b) + bool(c) <= 2.
Réintégrer Monica
6

Pour être clair, vous voulez prendre votre décision en fonction de la quantité de paramètres logiques TRUE (en cas d'arguments de chaîne - non vides)?

argsne = (1 if a else 0) + (1 if b else 0) + (1 if c else 0)

Ensuite, vous avez pris une décision:

if ( 0 < argsne < 3 ):
 doSth() 

Maintenant, la logique est plus claire.

Marin danubien
la source
5

Et pourquoi ne pas simplement les compter?

import sys
a = sys.argv
if len(a) = 1 :  
    # No arguments were given, the program name count as one
elif len(a) = 4 :
    # Three arguments were given
else :
    # another amount of arguments was given
Louis
la source
5

Si cela ne vous dérange pas d'être un peu énigmatique, vous pouvez simplement lancer 0 < (a + b + c) < 3ce qui reviendra truesi vous avez entre une et deux déclarations vraies et false si toutes sont fausses ou aucune n'est fausse.

Cela simplifie également si vous utilisez des fonctions pour évaluer les booléens car vous n'évaluez les variables qu'une seule fois et ce qui signifie que vous pouvez écrire les fonctions en ligne et que vous n'avez pas besoin de stocker temporairement les variables. (Exemple:. 0 < ( a(x) + b(x) + c(x) ) < 3)

Spinno
la source
4

La question indique que vous avez besoin des trois arguments (a et b et c) ou aucun d'entre eux (pas (a ou b ou c))

Cela donne:

(a et b et c) ou non (a ou b ou c)

Détente à Chypre
la source
4

Si je comprends bien, vous avez une fonction qui reçoit 3 arguments, mais si ce n'est pas le cas, elle fonctionnera avec le comportement par défaut. Puisque vous n'avez pas expliqué ce qui devrait se passer lorsque 1 ou 2 arguments sont fournis, je suppose qu'il devrait simplement faire le comportement par défaut. Dans ce cas, je pense que vous trouverez la réponse suivante très avantageuse:

def method(a=None, b=None, c=None):
    if all([a, b, c]):
        # received 3 arguments
    else:
        # default behavior

Cependant, si vous souhaitez que 1 ou 2 arguments soient traités différemment:

def method(a=None, b=None, c=None):
    args = [a, b, c]
    if all(args):
        # received 3 arguments
    elif not any(args):
        # default behavior
    else:
        # some args (raise exception?)

Remarque: cela suppose que les Falsevaleurs " " ne seront pas transmises à cette méthode.

Rose Inbar
la source
vérifier la valeur de vérité d'un argument est une question différente de vérifier si un argument est présent ou absent
wim
@wim So convertit une question en fonction de votre réponse. Le module argparse n'a rien à voir avec la question, il ajoute une autre importation, et si l'OP ne prévoit pas du tout d'utiliser argparse, il ne les aidera pas quoi que ce soit. De plus, si le "script" n'est pas autonome, mais un module, ou une fonction dans un plus grand ensemble de code, il peut déjà avoir un analyseur d'argument, et cette fonction particulière dans ce script plus grand peut être par défaut ou personnalisée. En raison des informations limitées de l'OP, je ne peux pas savoir comment la méthode devrait agir, mais il est prudent de supposer que l'OP ne passe pas de booléens.
Inbar Rose
La question disait explicitement "J'ai un script python qui peut recevoir zéro ou trois arguments en ligne de commande", elle n'a pas dit "J'ai une fonction qui reçoit 3 arguments". Puisque le module argparse est le moyen préféré de gérer les arguments de ligne de commande en python, il a automatiquement tout à voir avec la question. Enfin, python est "batteries incluses" - il n'y a aucun inconvénient à "ajouter une autre importation" lorsque ce module fait partie des bibliothèques standard.
wim
@wim La question n'est pas claire (le corps ne correspond pas au titre, par exemple). Je pense que la question n'est pas suffisamment claire pour que ce soit une réponse valable pour une certaine interprétation de celle-ci.
Réintégrer Monica
2

Si vous travaillez avec un itérateur de conditions, l'accès peut être lent. Mais vous n'avez pas besoin d'accéder à chaque élément plus d'une fois, et vous n'avez pas toujours besoin de tout lire. Voici une solution qui fonctionnera avec des générateurs infinis:

#!/usr/bin/env python3
from random import randint
from itertools import tee

def generate_random():
    while True:
        yield bool(randint(0,1))

def any_but_not_all2(s): # elegant
    t1, t2 = tee(s)
    return False in t1 and True in t2 # could also use "not all(...) and any(...)"

def any_but_not_all(s): # simple
    hadFalse = False
    hadTrue = False
    for i in s:
        if i:
            hadTrue = True
        else:
            hadFalse = True
        if hadTrue and hadFalse:
            return True
    return False


r1, r2 = tee(generate_random())
assert any_but_not_all(r1)
assert any_but_not_all2(r2)

assert not any_but_not_all([True, True])
assert not any_but_not_all2([True, True])

assert not any_but_not_all([])
assert not any_but_not_all2([])

assert any_but_not_all([True, False])
assert any_but_not_all2([True, False])
Janus Troelsen
la source
0

Quand chaque donné boolest True, ou quand chaque donné boolest False...
ils sont tous égaux l'un à l'autre!

Il suffit donc de trouver deux éléments qui s'évaluent à des bools différents
pour savoir qu'il y en a au moins un Trueet au moins un False.

Ma solution courte:

not bool(a)==bool(b)==bool(c)

Je crois qu'il court-circuits, car AFAIK a==b==cest égal a==b and b==c.

Ma solution généralisée:

def _any_but_not_all(first, iterable): #doing dirty work
    bool_first=bool(first)
    for x in iterable:
        if bool(x) is not bool_first:
            return True
    return False

def any_but_not_all(arg, *args): #takes any amount of args convertable to bool
    return _any_but_not_all(arg, args)

def v_any_but_not_all(iterable): #takes iterable or iterator
    iterator=iter(iterable)
    return _any_but_not_all(next(iterator), iterator)

J'ai également écrit du code traitant de plusieurs itérables, mais je l'ai supprimé d'ici car je pense que c'est inutile. Il est cependant toujours disponible ici .

GingerPlusPlus
la source
-2

Ceci est essentiellement un « certains (mais pas tous) » fonctionnalité (en contraste avec le any()etall() fonctions intégrées ).

Cela implique qu'il devrait y avoir des Falses et des True s parmi les résultats. Par conséquent, vous pouvez effectuer les opérations suivantes:

some = lambda ii: frozenset(bool(i) for i in ii).issuperset((True, False))

# one way to test this is...
test = lambda iterable: (any(iterable) and (not all(iterable))) # see also http://stackoverflow.com/a/16522290/541412

# Some test cases...
assert(some(()) == False)       # all() is true, and any() is false
assert(some((False,)) == False) # any() is false
assert(some((True,)) == False)  # any() and all() are true

assert(some((False,False)) == False)
assert(some((True,True)) == False)
assert(some((True,False)) == True)
assert(some((False,True)) == True)

Un avantage de ce code est que vous n'avez besoin de parcourir qu'une seule fois les éléments (booléens) résultants.

Un inconvénient est que toutes ces expressions de vérité sont toujours évaluées et ne font pas de court-circuit comme les opérateurs or/ and.

Abbafei
la source
1
Je pense que c'est une complication inutile. Pourquoi un frozenset au lieu d'un vieil ensemble ordinaire? Pourquoi .issupersetplutôt que de simplement vérifier la longueur 2, boolne peut pas renvoyer autre chose que True et False de toute façon. Pourquoi attribuer un lambda (lecture: fonction anonyme) à un nom au lieu d'utiliser simplement une def?
wim
1
la syntaxe lambda est plus logique pour certains. ils ont de toute façon la même longueur puisque vous en avez besoin returnsi vous l'utilisez def. Je pense que la généralité de cette solution est agréable. il n'est pas nécessaire de se limiter aux booléens, la question est essentiellement "comment puis-je m'assurer que tous ces éléments apparaissent dans ma liste". pourquoi setsi vous n'avez pas besoin de la mutabilité? plus d'immuabilité est toujours meilleure si vous n'avez pas besoin de la performance.
Janus Troelsen
@JanusTroelsen Vous avez raison! Voici quelques raisons pour lesquelles je l'ai fait de cette façon; cela me rend les choses plus faciles et plus claires. J'ai tendance à adapter Python à ma façon de coder :-).
Abbafei
mais cela ne fonctionnera pas sur des générateurs infinis: P voir ma réponse :) indice:tee
Janus Troelsen
@JanusTroelsen Je réalise ceci :-). En fait, je l'avais inversé (avec True / False dans l'ensemble et l'itérable dans le paramètre de méthode) au début, mais j'ai réalisé que cela ne fonctionnerait pas avec des générateurs infinis, et un utilisateur pourrait ne pas se rendre compte (car ce fait n'est pas (encore) mentionné dans la documentation pour les paramètres de méthode d'ensemble itérable), et au moins comme cela, il est évident que cela ne prendra pas d'itérateurs infinis. J'étais au courant itertools.teemais 1) je cherchais un one-liner qui était simple / assez petit pour justifier un copier-coller, 2) vous avez déjà donné une réponse qui utilise cette technique :-)
Abbafei