如何检查列表是否有且只有一个真值?

在 python 中,我有一个应该具有 只有一个真值(即 bool(value) is True)的列表。有什么聪明的办法来检查这个吗?现在,我只是遍历列表并手动检查:

def only1(l)
true_found = False
for v in l:
if v and not true_found:
true_found=True
elif v and true_found:
return False #"Too Many Trues"
return true_found

这看起来不够优雅,也不太像蟒蛇。有没有更聪明的方法来做到这一点?

36313 次浏览

It depends if you are just looking for the value True or are also looking for other values that would evaluate to True logically (like 11 or "hello"). If the former:

def only1(l):
return l.count(True) == 1

If the latter:

def only1(l):
return sum(bool(e) for e in l) == 1

since this would do both the counting and the conversion in a single iteration without having to build a new list.

if sum([bool(x) for x in list]) == 1

(Assuming all your values are booleanish.)

This would probably be faster just summing it

sum(list) == 1

although it may cause some problems depending on the data types in your list.

You can do:

x = [bool(i) for i in x]
return x.count(True) == 1

Or

x = map(bool, x)
return x.count(True) == 1

Building on @JoranBeasley's method:

sum(map(bool, x)) == 1
>>> l = [0, 0, 1, 0, 0]
>>> has_one_true = len([ d for d in l if d ]) == 1
>>> has_one_true
True

A one-line answer that retains the short-circuiting behavior:

from itertools import ifilter, islice


def only1(l):
return len(list(islice(ifilter(None, l), 2))) == 1

This will be significantly faster than the other alternatives here for very large iterables that have two or more true values relatively early.

ifilter(None, itr) gives an iterable that will only yield truthy elements (x is truthy if bool(x) returns True). islice(itr, 2) gives an iterable that will only yield the first two elements of itr. By converting this to a list and checking that the length is equal to one we can verify that exactly one truthy element exists without needing to check any additional elements after we have found two.

Here are some timing comparisons:

  • Setup code:

    In [1]: from itertools import islice, ifilter
    
    
    In [2]: def fj(l): return len(list(islice(ifilter(None, l), 2))) == 1
    
    
    In [3]: def david(l): return sum(bool(e) for e in l) == 1
    
  • Exhibiting short-circuit behavior:

    In [4]: l = range(1000000)
    
    
    In [5]: %timeit fj(l)
    1000000 loops, best of 3: 1.77 us per loop
    
    
    In [6]: %timeit david(l)
    1 loops, best of 3: 194 ms per loop
    
  • Large list where short-circuiting does not occur:

    In [7]: l = [0] * 1000000
    
    
    In [8]: %timeit fj(l)
    100 loops, best of 3: 10.2 ms per loop
    
    
    In [9]: %timeit david(l)
    1 loops, best of 3: 189 ms per loop
    
  • Small list:

    In [10]: l = [0]
    
    
    In [11]: %timeit fj(l)
    1000000 loops, best of 3: 1.77 us per loop
    
    
    In [12]: %timeit david(l)
    1000000 loops, best of 3: 990 ns per loop
    

So the sum() approach is faster for very small lists, but as the input list gets larger my version is faster even when short-circuiting is not possible. When short-circuiting is possible on a large input, the performance difference is clear.

One that doesn't require imports:

def single_true(iterable):
i = iter(iterable)
return any(i) and not any(i)

Alternatively, perhaps a more readable version:

def single_true(iterable):
iterator = iter(iterable)


# consume from "i" until first true or it's exhausted
has_true = any(iterator)


# carry on consuming until another true value / exhausted
has_another_true = any(iterator)


# True if exactly one true found
return has_true and not has_another_true

This:

  • Looks to make sure i has any true value
  • Keeps looking from that point in the iterable to make sure there is no other true value

The most verbose solution is not always the most unelegant solution. Therefore I add just a minor modification (in order to save some redundant boolean evaluations):

def only1(l):
true_found = False
for v in l:
if v:
# a True was found!
if true_found:
# found too many True's
return False
else:
# found the first True
true_found = True
# found zero or one True value
return true_found

Here are some timings for comparison:

# file: test.py
from itertools import ifilter, islice


def OP(l):
true_found = False
for v in l:
if v and not true_found:
true_found=True
elif v and true_found:
return False #"Too Many Trues"
return true_found


def DavidRobinson(l):
return l.count(True) == 1


def FJ(l):
return len(list(islice(ifilter(None, l), 2))) == 1


def JonClements(iterable):
i = iter(iterable)
return any(i) and not any(i)


def moooeeeep(l):
true_found = False
for v in l:
if v:
if true_found:
# found too many True's
return False
else:
# found the first True
true_found = True
# found zero or one True value
return true_found

My output:

$ python -mtimeit -s 'import test; l=[True]*100000' 'test.OP(l)'
1000000 loops, best of 3: 0.523 usec per loop
$ python -mtimeit -s 'import test; l=[True]*100000' 'test.DavidRobinson(l)'
1000 loops, best of 3: 516 usec per loop
$ python -mtimeit -s 'import test; l=[True]*100000' 'test.FJ(l)'
100000 loops, best of 3: 2.31 usec per loop
$ python -mtimeit -s 'import test; l=[True]*100000' 'test.JonClements(l)'
1000000 loops, best of 3: 0.446 usec per loop
$ python -mtimeit -s 'import test; l=[True]*100000' 'test.moooeeeep(l)'
1000000 loops, best of 3: 0.449 usec per loop

As can be seen, the OP solution is significantly better than most other solutions posted here. As expected, the best ones are those with short circuit behavior, especially that solution posted by Jon Clements. At least for the case of two early True values in a long list.

Here the same for no True value at all:

$ python -mtimeit -s 'import test; l=[False]*100000' 'test.OP(l)'
100 loops, best of 3: 4.26 msec per loop
$ python -mtimeit -s 'import test; l=[False]*100000' 'test.DavidRobinson(l)'
100 loops, best of 3: 2.09 msec per loop
$ python -mtimeit -s 'import test; l=[False]*100000' 'test.FJ(l)'
1000 loops, best of 3: 725 usec per loop
$ python -mtimeit -s 'import test; l=[False]*100000' 'test.JonClements(l)'
1000 loops, best of 3: 617 usec per loop
$ python -mtimeit -s 'import test; l=[False]*100000' 'test.moooeeeep(l)'
100 loops, best of 3: 1.85 msec per loop

I did not check the statistical significance, but interestingly, this time the approaches suggested by F.J. and especially that one by Jon Clements again appear to be clearly superior.

If there is only one True, then the length of the Trues should be one:

def only_1(l): return 1 == len(filter(None, l))

@JonClements` solution extended for at most N True values:

# Extend any() to n true values
def _NTrue(i, n=1):
for x in xrange(n):
if any(i): # False for empty
continue
else:
return False
return True


def NTrue(iterable, n=1):
i = iter(iterable)
return any(i) and not _NTrue(i, n)

edit: better version

def test(iterable, n=1):
i = iter(iterable)
return sum(any(i) for x in xrange(n+1)) <= n

edit2: include at least m True's and at most n True's

def test(iterable, n=1, m=1):
i = iter(iterable)
return  m <= sum(any(i) for x in xrange(n+1)) <= n

This seems to work and should be able to handle any iterable, not justlists. It short-circuits whenever possible to maximize efficiency. Works in both Python 2 and 3.

def only1(iterable):
for i, x in enumerate(iterable):  # check each item in iterable
if x: break                   # truthy value found
else:
return False                  # no truthy value found
for x in iterable[i+1:]:          # one was found, see if there are any more
if x: return False            #   found another...
return True                       # only a single truthy value found


testcases = [  # [[iterable, expected result], ... ]
[[                          ], False],
[[False, False, False, False], False],
[[True,  False, False, False], True],
[[False, True,  False, False], True],
[[False, False, False, True],  True],
[[True,  False, True,  False], False],
[[True,  True,  True,  True],  False],
]


for i, testcase in enumerate(testcases):
correct = only1(testcase[0]) == testcase[1]
print('only1(testcase[{}]): {}{}'.format(i, only1(testcase[0]),
'' if correct else
', error given '+str(testcase[0])))

Output:

only1(testcase[0]): False
only1(testcase[1]): False
only1(testcase[2]): True
only1(testcase[3]): True
only1(testcase[4]): True
only1(testcase[5]): False
only1(testcase[6]): False

Is this what you're looking for?

sum(l) == 1
def only1(l)
sum(map(lambda x: 1 if x else 0, l)) == 1

Explanation: The map function maps a list to another list, doing True => 1 and False => 0. We now have a list of 0s and 1s instead of True or False. Now we simply sum this list and if it is 1, there was only one True value.

import collections


def only_n(l, testval=True, n=1):
counts = collections.Counter(l)
return counts[testval] == n

Linear time. Uses the built-in Counter class, which is what you should be using to check counts.

Re-reading your question, it looks like you actually want to check that there is only one truthy value, rather than one True value. Try this:

import collections


def only_n(l, testval=True, coerce=bool, n=1):
counts = collections.Counter((coerce(x) for x in l))
return counts[testval] == n

While you can get better best case performance, nothing has better worst-case performance. This is also short and easy to read.

Here's a version optimised for best-case performance:

import collections
import itertools


def only_n(l, testval=True, coerce=bool, n=1):
counts = collections.Counter()
def iterate_and_count():
for x in itertools.imap(coerce,l):
yield x
if x == testval and counts[testval] > n:
break
counts.update(iterate_and_count())
return counts[testval] == n

The worst case performance has a high k (as in O(kn+c)), but it is completely general.

Here's an ideone to experiment with performance: http://ideone.com/ZRrv2m

I wanted to earn the necromancer badge, so I generalized the Jon Clements' excellent answer, preserving the benefits of short-circuiting logic and fast predicate checking with any and all.

Thus here is:

N(trues) = n

def n_trues(iterable, n=1):
i = iter(iterable)
return all(any(i) for j in range(n)) and not any(i)

N(trues) <= n:

def up_to_n_trues(iterable, n=1):
i = iter(iterable)
all(any(i) for j in range(n))
return not any(i)

N(trues) >= n:

def at_least_n_trues(iterable, n=1):
i = iter(iterable)
return all(any(i) for j in range(n))

m <= N(trues) <= n

def m_to_n_trues(iterable, m=1, n=1):
i = iter(iterable)
assert m <= n
return at_least_n_trues(i, m) and up_to_n_trues(i, n - m)

For completeness' sake and to demonstrate advanced use of Python's control flow for for loop iteration, one can avoid the extra accounting in the accepted answer, making this slightly faster.:

def one_bool_true(iterable):
it = iter(iterable)
for i in it:
if i:
break
else:            #no break, didn't find a true element
return False
for i in it:     # continue consuming iterator where left off
if i:
return False
return True      # didn't find a second true.

The above's simple control flow makes use of Python's sophisticated feature of loops: the else. The semantics are that if you finish iterating over the iterator that you are consuming without break-ing out of it, you then enter the else block.

Here's the accepted answer, which uses a bit more accounting.

def only1(l):
true_found = False
for v in l:
if v:
# a True was found!
if true_found:
# found too many True's
return False
else:
# found the first True
true_found = True
# found zero or one True value
return true_found

to time these:

import timeit
>>> min(timeit.repeat(lambda: one_bool_true([0]*100 + [1, 1])))
13.992251592921093
>>> min(timeit.repeat(lambda: one_bool_true([1, 1] + [0]*100)))
2.208037032979064
>>> min(timeit.repeat(lambda: only1([0]*100 + [1, 1])))
14.213872335107908
>>> min(timeit.repeat(lambda: only1([1, 1] + [0]*100)))
2.2482982632641324
>>> 2.2482/2.2080
1.0182065217391305
>>> 14.2138/13.9922
1.0158373951201385

So we see that the accepted answer takes a bit longer (slightly more than one and a half of a percent).

Naturally, using the built-in any, written in C, is much faster (see Jon Clement's answer for the implementation - this is the short form):

>>> min(timeit.repeat(lambda: single_true([0]*100 + [1, 1])))
2.7257133318785236
>>> min(timeit.repeat(lambda: single_true([1, 1] + [0]*100)))
2.012824866380015

Here's something that ought to work for anything truthy, though it has no short-circuit. I found it while looking for a clean way to forbid mutually-exclusive arguments:

if sum(1 for item in somelist if item) != 1:
raise ValueError("or whatever...")

What about:

len([v for v in l if type(v) == bool and v])

If you only want to count boolean True values.