ChatGPT解决这个技术问题 Extra ChatGPT

Python syntax for "if a or b or c but not all of them"

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

?

maybe start off with something like `if len(sys.argv) == 0:
@EdgarAroutiounian len(sys.argv) will always be at least 1: it includes the executable as argv[0].
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)?
What can you say about a+b+c?
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)?

S
Stefano Sanfilippo

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.


All great answers, but this wins for conciseness, with great short-circuiting. Thanks all!
I'd make it even more concise and go with if not (a and b and c) and (a or b or c)
Or even if (a or b or c) and not (a and b and c) to match the title perfectly ;)
@HennyH I believe the question asks for "at least one condition true but not all", not "only one condition true".
@Supr if any([a,b,c]) and not all([a,b,c])
d
defuz

How about:

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

Other variant:

if 1 <= sum(map(bool, conditions)) <= 2:
   ...

sum(conditions) can go wrong if any of them returns 2 for example, which is True.
True you would need an ugly sum(map(bool, conditions))
Note that this isn't short-circuiting, because all conditions are pre-evaluated.
@PaulScheltema The first form is more understandable for anyone.
This "any and not all" is the best and clearest of the boolean methods, just be mindful of the important distinction between an arg being present and an arg being 'truthy'
w
wim

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

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.
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 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 ... ;)
This is the only answer I upvoted. +1 for answering the real question.
+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.
J
Jon Clements

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)

I don't get it. It reads like: if True and not True. Help me understand.
@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".
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?
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.
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!
K
Kaz

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


Actually, that form is minimal. It's the minimal PoS form for the expression.
C
Casimir et Hippolyte

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

For the record, question asks for two conditions. At least one, but not all of them = 1 of all or 2 of all
IMHO you should spell the second one as 1 <= bool(a) + bool(b) + bool(c) <= 2.
e
eumiro

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:

Not as pretty as the answer by @defuz
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.
D
Danubian Sailor

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.


L
Louis

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

S
Spinno

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


R
Relaxing In Cyprus

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)


I
Inbar Rose

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.


checking the truth value of an argument is a different matter from checking whether an argument is present or absent
@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.
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 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.
J
Janus Troelsen

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

G
GingerPlusPlus

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.


A
Abbafei

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.


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?
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.
@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 :-).
but it won't work on infinite generators :P see my answer :) hint: tee
@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 :-)