142

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

?

Michael Currie
  • 13,721
  • 9
  • 42
  • 58
Chris Wilson
  • 6,599
  • 8
  • 35
  • 71
  • 4
    maybe start off with something like `if len(sys.argv) == 0: –  May 13 '13 at 12:32
  • 6
    @EdgarAroutiounian `len(sys.argv)` will always be at least 1: it includes the executable as `argv[0]`. – RoadieRich May 13 '13 at 13:49
  • 11
    The body of the question doesn't match the title of the question. Do you want to check "if a or b or c but not all of them" or "if exactly one of a, b, and c" (as the expression you gave does)? – Doug McClean May 13 '13 at 14:40
  • 2
    What can you say about a+b+c? – gukoff May 13 '13 at 15:06
  • try [this answer](http://stackoverflow.com/a/11036506/1370321) from an older SO question. – acolyte May 13 '13 at 16:24
  • 6
    Wait, question, it can take either zero or three arguments. couldn't you just say `if not (a and b and c)` (zero args), and then `if a and b and c` (all three args)? – acolyte May 13 '13 at 18:55
  • 2
    The title of the question and the description in natural language seem to be what the questioner wants. His pseudocode expresses a different logical condition, which is not what he wants. This is understandable since he is asking for help writing the expression. – jwg May 14 '13 at 08:04
  • So, the if is for case, when you have 1 or 2 non-zero variables? – Danubian Sailor May 14 '13 at 08:32
  • @acolyte: that's a bitwise operation, but stackoverflow.com/a/8158764 from the same question looks like the most elegant solution to me. – Wooble May 14 '13 at 10:22

15 Answers15

250

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.

Stefano Sanfilippo
  • 32,265
  • 7
  • 79
  • 80
  • 3
    All great answers, but this wins for conciseness, with great short-circuiting. Thanks all! – Chris Wilson May 13 '13 at 12:46
  • 38
    I'd make it even more concise and go with `if not (a and b and c) and (a or b or c)` – Volatility May 13 '13 at 12:52
  • 208
    Or even `if (a or b or c) and not (a and b and c)` to match the title perfectly ;) – Supr May 13 '13 at 13:22
  • 1
    @Stefano I don't think this works, if I understand this `cases = [[(True,True,True),False], [(True,True,False),False], [(True,False,True),False], [(False,True,True),False], [(True,False,False),True], [(False,False,True),True], [(False,True,False),True]]` Should cover all cases, When I ran a test using these and your solution I got 3 failures on `True,True,False`,`True,False,True`,`False,True,True`. – HennyH May 13 '13 at 13:31
  • 3
    @HennyH I believe the question asks for "at least one condition true but not all", not "only one condition true". – Volatility May 13 '13 at 13:32
  • @Volatility ahh, true true!, so only `True,True,True` or `False,False,False` should be false? – HennyH May 13 '13 at 13:34
  • 64
    @Supr `if any([a,b,c]) and not all([a,b,c])` – eternalmatt May 13 '13 at 19:06
  • Thats' why I love Python - so succinct. I love eternalmatt's comment. – duffymo May 13 '13 at 21:03
  • 1
    The expression provided in this answer is indeed minimal, but not at all easy to read. `any()` and `all()` built-ins would make the condition much neater, but anyway—considering context—it's a job for `argparse`. – Anton Strogonoff May 14 '13 at 01:07
  • 2
    This is not minimal, and neither does it exactly translate the title of the question as far as I can see – wim May 14 '13 at 03:01
  • @wim if you are not convinced by the translation, try building the truth table. – Stefano Sanfilippo May 14 '13 at 06:16
  • Sorry, I'm not talking about the translation at the logic level, rather than translation at the literal english level - ergo the number of upvotes on @Supr comment – wim May 14 '13 at 06:26
  • 1
    @eternalmatt, so the most literal translation becomes `if (a or b or c) and not all([a,b,c])` – Supr May 14 '13 at 08:38
  • 1
    @wim, if I was getting points for that comment then I would've felt bad; all I did was take Volatility's comment and swap the operands :P – Supr May 14 '13 at 08:41
  • @eternalmatt Your comment needs a lot more attention, the use of those built-in functions can greatly improve readability in cases with long comparison statements. I love it. – rob Dec 17 '15 at 17:01
243

How about:

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

Other variant:

if 1 <= sum(map(bool, conditions)) <= 2:
   ...
defuz
  • 26,721
  • 10
  • 38
  • 60
117

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
wim
  • 338,267
  • 99
  • 616
  • 750
  • Fair enough. But in the process, you added the `--foo` argument. We don't know whether that's compatible with the OP's requirements. A solution that relies on `argparse` but doesn't require the use of `--foo` would be a bigger win. – LarsH May 13 '13 at 15:37
  • 4
    Yes, I added that intentionally. It would be possible to make the argument positional, and enforce that exactly 3 or 0 are consumed, but it would not make a good CLI so I have not recommended it. – wim May 13 '13 at 15:40
  • 8
    Separate issue. You don't believe it's good CLI, and you can argue for that point, and the OP may be persuaded. But your answer deviates from the question significantly enough that the change of spec needs to be mentioned. You seem to be bending the spec to fit the available tool, without mentioning the change. – LarsH May 13 '13 at 18:27
  • 2
    @LarsH OK, I've added an example which fits better with the original interface implied in the question. Now it is bending the tool to meet the available spec ... ;) – wim May 14 '13 at 02:11
  • 2
    This is the only answer I upvoted. +1 for answering *the real question*. – Jonathon Reinhart May 14 '13 at 08:08
  • 1
    +1. The form of the CLI is an important tangential issue, not completely separate as another person said. I upvoted your post as well as others -- yours gets at the root of the problem and offers an elegant solution, whereas other posts answer the literal question. And both kinds of answers are useful and deserve +1. – Ben Lee May 14 '13 at 19:13
  • I agree that the guts of the question really is the ability to handle CLI, not the logical question of the title. Without the context provided by the text, it is a bit of an XY problem (http://xyproblem.info/). – Fred Mitchell Aug 20 '18 at 03:06
31

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)
Jon Clements
  • 138,671
  • 33
  • 247
  • 280
  • Clever indeed. I had to read it at least 4 times until I understood it. I think that some explanation might be welcomed. – rodrigo May 13 '13 at 12:46
  • 6
    I don't get it. It reads like: if True and not True. Help me understand. – rGil May 13 '13 at 12:47
  • 1
    @rGil: it reads like "if some apples are red, and some are not" - it's the same as saying "some apples are red, but not all of them". – georg May 13 '13 at 12:55
  • Ok. So if the first use of `any` stops on _a_ , does the second use of `any` begin on _b_ ? – rGil May 13 '13 at 12:58
  • 1
    Very cool. I would never have put that together on my own. So glad I read this post. Nice work @JonClements – rGil May 13 '13 at 13:02
  • 2
    Even with explanation I can't understand behavior... With `[a, b, c] = [True, True, False]` shouldn't your code "prints" `False`, while expected output is `True`? – awesoon May 13 '13 at 13:02
  • @soon yeah - I think half the answers are answering one question from the title of the OP and the other from the question - I think most are going for **only one** option, while others are going for 0, 1, 2 but not 3... – Jon Clements May 13 '13 at 13:04
  • @soon: Indeed, `any()` will first return `True`, the `True` again as the first and second elements are `True`, satisfying the `any()` premise. – Martijn Pieters May 13 '13 at 13:04
  • @soon I've added `not all(i) and any(i)` as alternative which covers the other way of allowing 1 or more, but not all - as opposed to the original, which was only one – Jon Clements May 13 '13 at 13:12
  • 1
    Looks like a try for optimization of a problem which doesn't need any optimization. `all` and `any` work perfectly fine for lists, this is just unreadable and probably not a slight bit faster. If you have long lists of arguments, you'll probably start to use some numerical library anyways. – Michael May 13 '13 at 13:48
  • 6
    This is pretty clever, BUT: I'd use this approach if you didn't know how many conditions you had up-front, but for a fixed, known list of conditionals, the loss of readability simply isn't worth it. – fluffy May 13 '13 at 14:02
  • @Michael: whether short-circuiting is faster or not depends a great deal on whether the conditions to be evaluated take a long time to compute. – LarsH May 13 '13 at 15:36
  • 4
    This doesn't short-circuit. The list is completely constructed before it's passed to `iter`. `any` and `all` will lazily consume the list, true, but the list was already completely evaluated by the time you get there! – icktoofay May 14 '13 at 03:39
  • 1
    See icktoofays point, if the computation of the iterated arguments is the bottleneck, it will compute all statements before iterating over them anyways. So it will be only shorter, if not the computation of the arguments takes long time, but only if there are lots of easily computable ones. Anyways, it's not a question which would be worth making much fuzz about it. – Michael May 15 '13 at 12:55
22

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

Kaz
  • 55,781
  • 9
  • 100
  • 149
10

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]):
Casimir et Hippolyte
  • 88,009
  • 5
  • 94
  • 125
9

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:
eumiro
  • 207,213
  • 34
  • 299
  • 261
  • Not as pretty as the answer by @defuz – jamylak May 13 '13 at 12:36
  • This checks that exactly one of the conditions is true. It returns False if 2 of the conditions are true, whereas "a or b or c but not all of them" should return True if two of the conditions are true. – Boris Verkhovskiy Oct 23 '20 at 03:07
7

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.

Danubian Sailor
  • 1
  • 38
  • 145
  • 223
5

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
Louis
  • 2,854
  • 2
  • 19
  • 24
5

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

Spinno
  • 177
  • 1
  • 5
4

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.

Inbar Rose
  • 41,843
  • 24
  • 85
  • 131
  • checking the truth value of an argument is a different matter from checking whether an argument is present or absent – wim May 14 '13 at 02:49
  • @wim So is converting a question to suit your answer. The argparse module has nothing to do with the question, it adds another import, and if the OP is not planning to use argparse at all, it wont help them what-so-ever. Also, if the "script" is not standalone, but a module, or a function within a larger set of code, it may already have an argument parser, and this particular function within that larger script can be default or customized. Due to limited information from the OP, I can not know how the method should act, but it is safe to assume the OP is not passing bools. – Inbar Rose May 14 '13 at 06:54
  • Question explicitly said "I have a python script that can receive either zero or three command line arguments", it did not say "I have a function that receives 3 arguments". Since the argparse module is the preferred way of handling command line arguments in python, it automatically has everything to do with the question. Lastly, python is "batteries included" - there isn't any downside with "adding another import" when that module is part of the standard libraries. – wim May 14 '13 at 07:02
  • @wim The question is quite unclear (body doesn't match title, for example). I think the question is unclear enough that this is a valid answer for *some* interpretation of it. – Reinstate Monica May 14 '13 at 21:21
4

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)

Relaxing In Cyprus
  • 1,976
  • 19
  • 25
2

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])
Janus Troelsen
  • 20,267
  • 14
  • 135
  • 196
0

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 bools
to know that there is at least one True and at least one False.

My short solution:

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

I belive it short-circuits, cause AFAIK a==b==c equals a==b and b==c.

My generalized solution:

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.

GingerPlusPlus
  • 5,336
  • 1
  • 29
  • 52
-2

This is basically a "some (but not all)" functionality (when contrasted with the any() and all() builtin functions).

This implies that there should be Falses and Trues 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.

Abbafei
  • 3,088
  • 3
  • 27
  • 24
  • 1
    I think this is needless complication. Why a frozenset instead of a plain old set? Why `.issuperset` rather than just checking for length 2, `bool` can't return anything else than True and False anyway. Why assign a lambda (read: anonymous function) to a name instead of just using a def? – wim May 14 '13 at 09:30
  • 1
    the lambda syntax is more logical to some. they're the same length anyway since you need `return` if you use `def`. i think the generality of this solution is nice. it's not necessary to restrict ourselves to booleans, the question essentially is "how do I ensure all of these elements occur in my list". why `set` if you don't need the mutability? more immutability is always better if you don't need the performance. – Janus Troelsen May 20 '13 at 00:35
  • @JanusTroelsen You're right on target! These are some reasons why I did it this way; it makes it easier and clearer to me. I tend to adapt Python to my way of coding :-). – Abbafei May 22 '13 at 10:24
  • but it won't work on infinite generators :P see my answer :) hint: `tee` – Janus Troelsen May 22 '13 at 19:26
  • @JanusTroelsen I realize this :-). I actually had it the other way around (with True/False in the set and the iterable in the method param) at first, but realized that this would not work with infinite generators, and a user might not realize (since this fact is not (yet) mentioned in the docs for iterable set method params), and at least like this it is obvious that it will not take infinite iterators. I was aware of `itertools.tee` but 1) I was looking for a one-liner which was simple/small enough to warrant copy-pasting, 2) you already gave an answer which uses that technique :-) – Abbafei May 24 '13 at 01:21