Reputation: 6719
I have a python script that can receive either zero or three command line arguments. (Either it runs on default behavior or needs all three values specified.)
What's the ideal syntax for something like:
if a and (not b or not c) or b and (not a or not c) or c and (not b or not a):
?
Upvotes: 142
Views: 292841
Reputation: 363354
This question already had many highly upvoted answers and an accepted answer, but all of them so far were distracted by various ways to express the boolean problem and missed a crucial point:
I have a python script that can receive either zero or three command line arguments. (Either it runs on default behavior or needs all three values specified)
This logic should not be the responsibility of library code in the first place, rather it should be handled by the command-line parsing (usually argparse
module in Python). Don't bother writing a complex if statement, instead prefer to setup your argument parser something like this:
#!/usr/bin/env python
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--foo', nargs=3, default=['x', 'y', 'z'])
args = parser.parse_args()
print(args.foo)
And yes, it should be an option not a positional argument, because it is after all optional.
edited: To address the concern of LarsH in the comments, below is an example of how you could write it if you were certain you wanted the interface with either 3 or 0 positional args. I am of the opinion that the previous interface is better style (because optional arguments should be options), but here's an alternative approach for the sake of completeness. Note we're overriding kwarg usage
when creating your parser, because argparse
will auto-generate a misleading usage message otherwise!
#!/usr/bin/env python
import argparse
parser = argparse.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)
Here are some usage examples:
# default case
$ ./three_or_none.py
['x', 'y', 'z']
# explicit case
$ ./three_or_none.py 1 2 3
['1', '2', '3']
# example failure mode
$ ./three_or_none.py 1 2
usage: three_or_none.py [-h] [a b c]
three_or_none.py: error: expected 3 arguments
Upvotes: 117
Reputation: 5636
When every given bool
is True
, or when every given bool
is False
...
they all are equal to each other!
So, we just need to find two elements which evaluates to different bool
s
to know that there is at least one True
and at least one False
.
not bool(a)==bool(b)==bool(c)
I belive it short-circuits, cause AFAIK a==b==c
equals a==b and b==c
.
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)
I wrote also some code dealing with multiple iterables, but I deleted it from here because I think it's pointless. It's however still available here.
Upvotes: 0
Reputation: 21298
If you work with an iterator of conditions, it could be slow to access. But you don't need to access each element more than once, and you don't always need to read all of it. Here's a solution that will work with infinite generators:
#!/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])
Upvotes: 2
Reputation: 177
If you don't mind being a bit cryptic you can simly roll with 0 < (a + b + c) < 3
which will return true
if you have between one and two true statements and false if all are false or none is false.
This also simplifies if you use functions to evaluate the bools as you only evaluate the variables once and which means you can write the functions inline and do not need to temporarily store the variables. (Example: 0 < ( a(x) + b(x) + c(x) ) < 3
.)
Upvotes: 5
Reputation: 2900
And why not just count them ?
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
Upvotes: 5
Reputation: 33096
If you mean a minimal form, go with this:
if (not a or not b or not c) and (a or b or c):
Which translates the title of your question.
UPDATE: as correctly said by Volatility and Supr, you can apply De Morgan's law and obtain equivalent:
if (a or b or c) and not (a and b and c):
My advice is to use whichever form is more significant to you and to other programmers. The first means "there is something false, but also something true", the second "There is something true, but not everything". If I were to optimize or do this in hardware, I would choose the second, here just choose the most readable (also taking in consideration the conditions you will be testing and their names). I picked the first.
Upvotes: 251
Reputation: 3136
This is basically a "some (but not all)" functionality (when contrasted with the any()
and all()
builtin functions).
This implies that there should be False
s and True
s among the results. Therefore, you can do the following:
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)
One advantage of this code is that you only need to iterate once through the resulting (booleans) items.
One disadvantage is that all these truth-expressions are always evaluated, and do not do short-circuiting like the or
/and
operators.
Upvotes: -2
Reputation: 22867
To be clear, you want to made your decision based on how much of the parameters are logical TRUE (in case of string arguments - not empty)?
argsne = (1 if a else 0) + (1 if b else 0) + (1 if c else 0)
Then you made a decision:
if ( 0 < argsne < 3 ):
doSth()
Now the logic is more clear.
Upvotes: 7
Reputation: 43487
As I understand it, you have a function that receives 3 arguments, but if it does not it will run on default behavior. Since you have not explained what should happen when 1 or 2 arguments are supplied I will assume it should simply do the default behavior. In which case, I think you will find the following answer very advantageous:
def method(a=None, b=None, c=None):
if all([a, b, c]):
# received 3 arguments
else:
# default behavior
However, if you want 1 or 2 arguments to be handled differently:
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?)
note: This assumes that "False
" values will not be passed into this method.
Upvotes: 4
Reputation: 58627
The English sentence:
“if a or b or c but not all of them”
Translates to this logic:
(a or b or c) and not (a and b and c)
The word "but" usually implies a conjunction, in other words "and". Furthermore, "all of them" translates to a conjunction of conditions: this condition, and that condition, and other condition. The "not" inverts that entire conjunction.
I do not agree that the accepted answer. The author neglected to apply the most straightforward interpretation to the specification, and neglected to apply De Morgan's Law to simplify the expression to fewer operators:
not a or not b or not c -> not (a and b and c)
while claiming that the answer is a "minimal form".
Upvotes: 22
Reputation: 2016
The question states that you need either all three arguments (a and b and c) or none of them (not (a or b or c))
This gives:
(a and b and c) or not (a or b or c)
Upvotes: 4
Reputation: 89629
What about: (unique condition)
if (bool(a) + bool(b) + bool(c) == 1):
Notice, if you allow two conditions too you could do that
if (bool(a) + bool(b) + bool(c) in [1,2]):
Upvotes: 10
Reputation: 142216
I'd go for:
conds = iter([a, b, c])
if any(conds) and not any(conds):
# okay...
I think this should short-circuit fairly efficiently
Explanation
By making conds
an iterator, the first use of any
will short circuit and leave the iterator pointing to the next element if any item is true; otherwise, it will consume the entire list and be False
. The next any
takes the remaining items in the iterable, and makes sure than there aren't any other true values... If there are, the whole statement can't be true, thus there isn't one unique element (so short circuits again). The last any
will either return False
or will exhaust the iterable and be True
.
note: the above checks if only a single condition is set
If you want to check if one or more items, but not every item is set, then you can use:
not all(conds) and any(conds)
Upvotes: 31
Reputation: 213055
This returns True
if one and only one of the three conditions is True
. Probably what you wanted in your example code.
if sum(1 for x in (a,b,c) if x) == 1:
Upvotes: 9
Reputation: 27611
How about:
conditions = [a, b, c]
if any(conditions) and not all(conditions):
...
Other variant:
if 1 <= sum(map(bool, conditions)) <= 2:
...
Upvotes: 243