关于python:如何检查列表是否只有一个truthy值?

How can I check that a list has one and only one truthy value?

在Python中,我有一个列表,该列表应该只有一个Truthy值(即,bool(value) is True)。有没有聪明的方法来检查这个?现在,我只是遍历列表并手动检查:

1
2
3
4
5
6
7
8
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

这看起来不太漂亮,也不是很像Python。有更聪明的方法吗?


不需要进口的:

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

或者,可能是更可读的版本:

1
2
3
4
5
def single_true(iterable):
    iterator = iter(iterable)
    has_true = any(iterator) # consume from"i" until first true or it's exhuasted
    has_another_true = any(iterator) # carry on consuming until another true value / exhausted
    return has_true and not has_another_true # True if exactly one true found

这是:

  • 确保i有任何真正的价值
  • 从ITerable中的这一点继续查看,以确保没有其他真正的值


这取决于您是只查找True值,还是同时查找逻辑上可对True进行评估的其他值(如11"hello")。如果前者:

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

如果后者:

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

因为这将在一次迭代中同时进行计数和转换,而不必构建一个新的列表。


最冗长的解决方案并不总是最不合理的解决方案。因此,我只添加了一个小修改(为了保存一些冗余的布尔值计算):

1
2
3
4
5
6
7
8
9
10
11
12
13
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

以下是一些比较时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 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

我的输出:

1
2
3
4
5
6
7
8
9
10
$ 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

如图所示,OP解决方案明显优于这里发布的大多数其他解决方案。正如所料,最好的是那些有短路行为的,尤其是乔恩·克莱门茨发布的解决方案。至少对于长列表中的两个早期True值的情况。

在这里,没有True值的情况也一样:

1
2
3
4
5
6
7
8
9
10
$ 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

我没有检查统计学的显著性,但有趣的是,这次F.J.提出的方法,特别是乔恩·克莱门茨提出的方法,显然更优越。


保持短路行为的一行应答:

1
2
3
4
from itertools import ifilter, islice

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

对于具有两个或两个以上真实值的非常大的iterables,这将比这里的其他替代方案快得多。

ifilter(None, itr)给出了一个只产生真实元素的iterable(如果bool(x)返回Truex是真实的)。islice(itr, 2)给出了一个只产生itr的前两个元素的iterable。通过将其转换为一个列表并检查长度是否等于一,我们可以验证只有一个真正的元素存在,而无需在找到两个元素之后再检查任何其他元素。

以下是一些时间比较:

  • 设置代码:

    1
    2
    3
    4
    5
    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
  • 表现出短路行为:

    1
    2
    3
    4
    5
    6
    7
    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

  • 不发生短路的大列表:

    1
    2
    3
    4
    5
    6
    7
    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

  • 小列表:

    1
    2
    3
    4
    5
    6
    7
    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

因此,对于非常小的列表,sum()方法更快,但是随着输入列表变大,即使不可能发生短路,我的版本也更快。当大输入可能发生短路时,性能差异是明显的。


我想赢得"巫师"徽章,所以我概括了乔恩·克莱门茨的优秀答案,保留了短路逻辑和快速谓词检查的优点。

因此,这里是:

n(trues)=n

1
2
3
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:

1
2
3
4
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:

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

m<=n(trues)<=n

1
2
3
4
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)


1
2
3
4
>>> l = [0, 0, 1, 0, 0]
>>> has_one_true = len([ d for d in l if d ]) == 1
>>> has_one_true
True


这似乎是可行的,应该能够处理任何不可行的,而不是justlists。它尽可能地短路,以最大限度地提高效率。适用于python 2和3。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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])))

输出:

1
2
3
4
5
6
7
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


你可以这样做:

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

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

基于@joranbeasley的方法:

1
sum(map(bool, x)) == 1


如果只有一个True,那么Trues的长度应为一:

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


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

(假设你所有的价值观都是虚伪的。)

这可能会更快,只要求和它

1
sum(list) == 1

尽管根据列表中的数据类型,它可能会导致一些问题。


@joncements的解决方案最多可扩展n个真值:

1
2
3
4
5
6
7
8
9
10
11
12
# 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)

编辑:更好的版本

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

edit2:至少包含m true和至多n true

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


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

说明:map函数将一个列表映射到另一个列表,执行True => 1False => 0操作。我们现在有一个0和1的列表,而不是真或假。现在我们简单地求和这个列表,如果它是1,就只有一个真值。


为了完整起见,为了演示python的控制流在for循环迭代中的高级使用,可以避免在接受的答案中使用额外的计算,从而使这个过程稍微快一点。

1
2
3
4
5
6
7
8
9
10
11
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.

上面简单的控制流利用了python复杂的循环特性:else。语义是,如果在没有使用break的情况下完成对迭代器的迭代,那么就进入else块。

这是公认的答案,它使用了更多的会计方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
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

对这些进行计时:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

所以我们看到,被接受的答案需要更长的时间(稍微超过百分之一点五)。

当然,使用C语言编写的内置any要快得多(参见Jon Clement关于实现的答案-这是一个简短的形式):

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


怎么办:

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

如果只想计算布尔真值。


这就是你要找的吗?

1
sum(l) == 1


1
2
3
4
5
import collections

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

线性时间。使用内置的counter类,这是您应该用来检查计数的。

重新阅读你的问题,看起来你真的想要检查只有一个真实的值,而不是一个True值。试试这个:

1
2
3
4
5
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

虽然您可以获得更好的最佳情况性能,但没有任何东西具有更好的最坏情况性能。这也很短,很容易阅读。

下面是一个优化的版本,以获得最佳案例性能:

1
2
3
4
5
6
7
8
9
10
11
12
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

最坏情况下的性能有很高的k(如O(kn+c)),但它是完全通用的。

下面是一个Ideone的性能实验:http://ideone.com/zrrv2m


这里有一些东西,应该为任何真实的工作,尽管它没有短路。我在寻找一种明确的方法来禁止相互排斥的论点时发现了这一点:

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