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

97
在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

这看起来不太优雅,也不太符合 Python 的风格。有没有更巧妙的方法?


2
我认为你的解决方案很好,而且很符合 Python 的风格! - wim
1
通用Lisp:(= 1 (count-if #'identity list)) - Kaz
9
列表lst所有元素之和等于1。 - Pål GD
请明确一下:您是想检查是否只有一个 True 或只有一个 truthy 值吗? - Marcin
@PålGD sum([0.5, 0.5]) - naught101
17个回答

304

一个不需要导入的方法:

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

或许更易读的版本:

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

这个函数:

  • 检查 i 是否为真值(true value)
  • 从可迭代对象的这一点开始继续查找,以确保没有其他真值存在

35
@MatthewScouten 不是的... 我们在这里从可迭代对象中获取元素... 试着运行代码... - Jon Clements
13
根据可迭代对象的消耗,any 函数会在找到第一个非假(即真)值时返回 True。在此之后,我们再次寻找真值,如果找到则将其视为失败......因此,这对于空列表、列表/其他序列和任何可迭代对象都适用...... - Jon Clements
13
副作用会破坏所有定理!仅当“x”是引用透明的时,“x and not x = False”才是正确的。请注意,本翻译不改变原意,仅使内容更加通俗易懂。 - Ben
16
@wim,这不是 any() 的实现细节——它是函数的已记录特性,并且保证符合 Python 规范的所有实现都具有该功能。 - Gareth Latty
19
任何认为这不是一个易读的解决方案的人都应该考虑以下:它简洁明了,仅依赖于Python已知的行为和常用结构。仅因为新手可能无法理解它,并不意味着它不易读。这也是教授应该知道的内容的绝佳方式,因为它激发了那些看不出其工作原理的人的即时好奇心。 - dansalmo
显示剩余29条评论

55

这取决于您是只寻找True值,还是也要寻找逻辑上评估为True的其他值(比如11"hello")。如果是前者:

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

如果是后者:

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

由于这样可以在单次迭代中同时进行计数和转换,而无需构建新列表。


3
在Python 3中:list(map(bool, l)).count(True) - poke
7
提醒原帖作者,当发现多个“True”值时,这段代码可能不会短路,所以在某些情况下可能会提高效率。 - NominSim
2
第二个函数可以写成return sum(bool(e) for e in l) == 1boolint的子类,True/False在算术运算中的行为类似于1/0。 - user395760
@delnan:谢谢,已经做出了那个更改。 - David Robinson
1
我会避免使用 l 作为变量名(它看起来太像 1 了),并且我会重写 sum(bool(e) for e in l)sum(1 for e in l if e) - wim
显示剩余2条评论

47

最冗长的解决方案并不总是最不优美的解决方案。因此,我只添加了一个小修改(以便节省一些冗余的布尔评估):

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

以下是一些时间的比较:

# 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

我的输出:

$ 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的解决方案比这里发布的大多数其他解决方案都要好得多。正如预期的那样,最好的解决方案是具有短路行为的解决方案,尤其是Jon Clements发布的那个解决方案。至少对于长列表中有两个早期的True值的情况是如此。

对于完全没有True值的情况,也是一样的:

$ 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. 建议的方法,特别是 Jon Clements 提出的那个,似乎明显优于其他方法。


4
看起来根据早期的真实时间,0.446 不是最快的吗?我需要进行翻译,请确认是否正确。 - Jon Clements
2
@JonClements 这就是为什么我写了“大多数”,现在更清楚了。(指的是发布的大多数,而不是测试的大多数...) - moooeeeep
1
我怀疑JonClement之所以如此快,是因为大多数any都是用C实现的。 - Matthew Scouten
1
+1 对于你的开场白。所有使用 sum 的答案实际上都比原始帖子的简单直接的代码更差。 - wim
2
@MarkAmery 我增加了关于可读性和优雅性(虽然很短)以及性能评估的部分。由于问题要求考虑巧妙性,我认为这两个方面都应该受到考虑。在我看来,我已经提供了一个回答来解决这两个相关方面的问题。如果您觉得这个答案没有用处,请随意点踩。 - moooeeeep
显示剩余2条评论

24
一个保留短路行为的一行回答:
from itertools import ifilter, islice

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

对于非常大的可迭代对象,其中有两个或更多值相对较早地为真,此方法将比其他替代方法显着更快。

ifilter(None, itr) 会产生一个可迭代对象,该对象仅产生真实元素(如果 bool(x) 返回 Truex 为真)。islice(itr, 2) 会产生一个可迭代对象,该对象仅产生 itr 的前两个元素。通过将其转换为列表并检查长度是否等于1,我们可以验证恰好存在一个真实元素,而无需在找到两个元素后检查任何其他元素。

以下是一些时间比较:

  • 设置代码:

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
  • 表现出短路行为:

    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
    
  • 短路不会发生的大型列表:

    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
    
  • 小列表:

    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()方法更快,但是随着输入列表变得越来越大,即使不能进行短路计算,我的版本也比其快。 当在大型输入上可以进行短路计算时,性能差异很明显。


    6
    哎呀,我花了三倍的时间才理解比起其他选项。如果短路计算很重要的话,我会选择原作者的代码,因为它更加明显,而且效率大致相同。 - user395760
    1
    点赞风格,保留短路计算。但这样更难阅读。 - Matthew Scouten
    1
    +1. 唯一一个完全复制 OP 的短路意图的方法。 - NominSim
    1
    如果您提供一些 timeit 实验来客观地进行性能比较,那么将会非常有帮助。 - moooeeeep
    如果你有一个无限可迭代对象,在其中某处有两个“true”值,这将会结束循环,而其他答案将永远旋转并尝试获取计数。 - NominSim
    显示剩余2条评论

    15

    我想获得死灵法师徽章,因此我推广了Jon Clements的优秀答案,保留了短路逻辑和使用任何和所有的快速谓词检查的好处。

    因此这里是:

    N(真值)= 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(真值)≥ 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)
    

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

    4
    为什么会被踩票?我认为这是最简单和易读的。 - dansalmo
    1
    @dansalmo:当然很难确定,但我的理论是许多n00b Python程序员 - 尤其是那些有Java背景的人 - 对更长的语法感到更舒适。(我自己5-10年前也有点这样,但今天我认为这是不专业和无知的。)+1 - Jonas Byström

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

    (假设你的所有值都是布尔类型。)这可能只需将它们求和即可更快地完成。
    sum(list) == 1   
    

    尽管这可能会因列表中的数据类型而导致一些问题。

    1
    这里需要一些大写字母和标点符号。 - Steven Rumbalski

    5
    你可以做以下事情:
    x = [bool(i) for i in x]
    return x.count(True) == 1
    

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

    在@JoranBeasley的方法基础上进行改进:

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

    4

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

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

    3
    你能解释一下你的回答吗? - Linus Caldwell

    4

    这个看起来可以工作,并且应该能处理任何可迭代对象,不仅限于list。它会尽可能地短路以最大化效率。在Python 2和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])))
    

    输出:

    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
    

    我喜欢这种方法,如何重新围绕iter(x for x in my_list if x)的逻辑,并使用next,也许比使用maplist.index更好。 - wim
    @wim:虽然我没有使用你建议的方法,但是你的评论激发了我修改原来的答案,使它更加增量式,并且摆脱了 maplist.index - martineau

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接