惯用Python has_one

时间:2009-10-17 22:23:16

标签: python idioms

我刚刚发明了一个愚蠢的小助手功能:

def has_one(seq, predicate=bool):
    """Return whether there is exactly one item in `seq` that matches
    `predicate`, with a minimum of evaluation (short-circuit).
    """
    iterator = (item for item in seq if predicate(item))
    try:
        iterator.next()
    except StopIteration: # No items match predicate.
        return False
    try:
        iterator.next()
    except StopIteration: # Exactly one item matches predicate.
        return True
    return False # More than one item matches the predicate.

因为我能想出的最可读/惯用的内联事物是:

[predicate(item) for item in seq].count(True) == 1

...这在我的情况下很好,因为我知道seq很小,但它只是感觉很奇怪。 我在这里忘记了一个成语,这使我不得不打破这个助手吗?

澄清

回过头来看,这是一个令人讨厌的问题,尽管我们得到了一些很好的答案!我一直在寻找:

  • 一个明显可读的内联习语或stdlib函数,在这种情况下,急切的评估是可以接受的。
  • 一个更明显和可读的辅助函数 - 因为它打破了整个其他函数,只有最小的评估量似乎是可以接受的。

@Stephan202为辅助函数提出了一个非常酷的习惯用法,@Martin v. Löwis在假设谓词返回bool的情况下提出了一个更简单的内联习语。感谢@大家的帮助!

8 个答案:

答案 0 :(得分:10)

如何在迭代器(Python 2.x和3.x兼容)上调用any两次?

>>> def has_one(seq, predicate=bool):
...     seq = (predicate(e) for e in seq)
...     return any(seq) and not any(seq)
... 
>>> has_one([])
False
>>> has_one([1])
True
>>> has_one([0])
False
>>> has_one([1, 2])
False

any最多需要一个元素,该元素从迭代器计算为True。如果它第一次成功并且第二次失败,则只有一个元素与谓词匹配。

编辑:我看到Robert Rossney建议使用通用版本,它会检查 n 元素是否与谓词匹配。让我参与其中,使用all

>>> def has_n(seq, n, predicate=bool):
...     seq = (predicate(e) for e in seq)
...     return all(any(seq) for _ in range(n)) and not any(seq)
... 
>>> has_n(range(0), 3)
False
>>> has_n(range(3), 3)
False
>>> has_n(range(4), 3)
True
>>> has_n(range(5), 3)
False

答案 1 :(得分:3)

也许这样的事情更符合你的口味?

def has_one(seq,predicate=bool):
    nwanted=1
    n=0
    for item in seq:
        if predicate(item):
            n+=1
            if n>nwanted:
                return False

    return n==nwanted

这与列表推导示例非常相似,但只需要对一个序列进行一次传递。与第二个has_one函数相比,并且与列表推导代码类似,它更容易概括为其他计数。我已经通过在变量中添加所需的项目来证明这一点(希望没有错误...)。

答案 2 :(得分:3)

我喜欢Stephan202的答案,但我更喜欢这个,即使它是两行而不是一行。我喜欢它,因为它同样疯狂,但更为明确的是它的疯狂如何运作:

def has_one(seq):
    g = (x for x in seq)
    return any(g) and not any(g)

修改

这是一个支持谓词的更通用的版本:

def has_exactly(seq, count, predicate = bool):
    g = (predicate(x) for x in seq)
    while(count > 0):
        if not any(g):
            return False
        count -= 1
    if count == 0:
        return not any(g)

答案 3 :(得分:2)

不确定它是否比你提出的版本更好,但是......

如果保证谓词仅返回True / False,那么

sum(map(predicate, seq)) == 1

会做(虽然它不会停在第二个元素)

答案 4 :(得分:1)

怎么样......

import functools
import operator

def exactly_one(seq):
    """
    Handy for ensuring that exactly one of a bunch of options has been set.
    >>> exactly_one((3, None, 'frotz', None))
    False
    >>> exactly_one((None, None, 'frotz', None))
    True
    """
    return 1 == functools.reduce(operator.__add__, [1 for x in seq if x])

答案 5 :(得分:1)

看,马!没有rtfm(“itertools”),没有依赖于谓词()返回布尔值,最小评估,只是工作!

Python 1.5.2 (#0, Apr 13 1999, 10:51:12) [MSC 32 bit (Intel)] on win32
Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
>>> def count_in_bounds(seq, predicate=lambda x: x, low=1, high=1):
...     count = 0
...     for item in seq:
...         if predicate(item):
...             count = count + 1
...             if count > high:
...                 return 0
...     return count >= low
...
>>> seq1 = [0, 0, 1, 0, 1, 0, 1, 0, 0, 0]
>>> count_in_bounds(seq1)
0
>>> count_in_bounds(seq1, low=3, high=3)
1
>>> count_in_bounds(seq1, low=3, high=4)
1
>>> count_in_bounds(seq1, low=4, high=4)
0
>>> count_in_bounds(seq1, low=0, high=3)
1
>>> count_in_bounds(seq1, low=3, high=3)
1
>>>

答案 6 :(得分:0)

这是修改后的@Stephan202's answer

from itertools import imap, repeat

def exactly_n_is_true(iterable, n, predicate=None):
    it = iter(iterable) if predicate is None else imap(predicate, iterable)
    return all(any(it) for _ in repeat(None, n)) and not any(it)

的差异:

  1. predicate()默认为None。其含义与内置filter()和stdlib的itertools.ifilter()函数相同。

  2. 更明确的功能和参数名称(这是主观的)。

  3. repeat()允许使用大型n

  4. 示例:

    if exactly_n_is_true(seq, 1, predicate):
       # predicate() is true for exactly one item from the seq
    

答案 7 :(得分:0)

Thisthis简单的计数循环解决方案绝对是最清晰的。

对于它的运动,这里有一个any(g) and not any(g)主题的变体,表面看起来不那么神奇 - 但是当调试/修改它时它实际上同样脆弱(你不能交换命令,您必须了解短路and如何将两个短路用户之间的单个迭代器移交......)。

def cumulative_sums(values):
    s = 0
    for v in values:
        s += v
        yield s

def count_in_bounds(iterable, start=1, stop=2):
    counter = cumulative_sums(bool(x) for x in iterable)
    return (start in counter) and (stop not in counter)

使用谓词而不是bool也是微不足道的,但我认为最好跟随any()all()将其留给调用者 - 如果将它传递给生成器,则很容易需要的。

任意[开始,停止]是一个很好的奖励,但它并不像我想的那样通用。很有可能通过stop=None来模仿,例如any(),它起作用,但总是消耗所有输入;适当的模拟有点尴尬:

def any(iterable):
  return not count_in_bounds(iterable, 0, 1)

def all(iterable):
  return count_in_bounds((not x for x in iterable), 0, 1)

获取可变数量的边界并指定哪些应返回True / False将失控 也许一个简单的饱和计数器是最好的原语:

def count_true(iterable, stop_at=float('inf')):
    c = 0
    for x in iterable:
        c += bool(x)
        if c >= stop_at:
            break
    return c

def any(iterable):
    return count_true(iterable, 1) >= 1

def exactly_one(iterable):
    return count_true(iterable, 2) == 1

def weird(iterable):
    return count_true(iterable, 10) in {2, 3, 5, 7}

all()仍需要否定输入或匹配的count_false()帮助器。