tags:

views:

300

answers:

7

I just invented a stupid little helper function:

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.

Because the most readable/idiomatic inline thing I could come up with was:

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

... which is fine in my case because I know seq is small, but it just feels weird. Is there an idiom I’m forgetting here that prevents me from having to break out this helper?

Clarification

Looking back on it, this was kind of a crappily posed question, though we got some excellent answers! I was looking for either:

  • An obvious and readable inline idiom or stdlib function, eager evaluation being acceptable in this case.
  • A more obvious and readable helper function -- since it's breaking out a whole other function, only the minimum amount of evaluation seems acceptable.

@Stephan202 came up with a really cool idiom for the helper function and @Martin v. Löwis came up with a more simple inline idiom under the assumption that the predicate returns a bool. Thanks @ everybody for your help!

+2  A: 

Not sure whether it is any better than the versions you proposed, however...

If predicate is guaranteed to return True/False only, then

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

will do (although it won't stop at the second element)

Martin v. Löwis
i think stopping execution at the second item is the whole point of this exercise
SilentGhost
@SilentGhost: I'm unsure. The version that he posted with the list comprehension doesn't; he just says it feels "weird" (for unspecified reasons).
Martin v. Löwis
I like this better than the question's .count(True) for the inline idiom. The corresponding generator expression is uglier in this particular case, as sum(1 for item in seq if predicate(item)), so I think map is the way to go. Technically, though, the genexp doesn't rely on the predicate returning a bool.
cdleary
+2  A: 

Perhaps something like this is more to your taste?

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

This is rather like the list comprehension example, but requires only one pass over one sequence. Compared to the second has_one function, and like the list comprehension code, it generalizes more easily to other counts. I've demonstrated this (hopefully without error...) by adding in a variable for the number of items wanted.

brone
+10  A: 

How about calling any twice, on an iterator (Python 2.x and 3.x compatible)?

>>> 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 will take at most one element which evaluates to True from the iterator. If it succeeds the first time and fails the second time, then only one element matches the predicate.

Edit: I see Robert Rossney suggests a generalized version, which checks whether exactly n elements match the predicate. Let me join in on the fun, using 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
Stephan202
I like it, another clever thing to do with iterators.
THC4k
for python2 just use itertools.imap instead of map
gnibbler
even better is `seq = (x for x in seq if predicate(x))` since the 2.x `filter` is eager.
THC4k
@THC4K: yes, but we need a `map`, not a `filter`. Updated the answer (no more explicit `map`).
Stephan202
I really like this, but it would be nice if the `n` parameter had a default of 1 IMHO.
Chris Lutz
+3  A: 

I liked Stephan202's answer, but I like this one a little more, even though it's two lines instead of one. I like it because it's just as crazy but a tiny bit more explicit about how its craziness works:

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

Edit:

Here's a more generalized version that supports a predicate:

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)
Robert Rossney
It's clever, but I think that's probably a bad thing. I wouldn't blame people for looking at it concluding it never returns true.
cdleary
I like this one too, but it does not allow a predicate to be passed. Anyway, given that, how about `g = iter(seq)`?
Stephan202
@cdleary, isn't that part of being an idiom in this case?
gnibbler
@gnibbler: You could probably make a case for it, but I don't particularly like idioms that seem to violate fundamental laws of logic. (Law of the excluded middle is slightly more universal than Python ;-)
cdleary
I'd certainly add a comment explaining what it's doing in production code.
Robert Rossney
Note that this code fails if the predicate is e.g. `lambda x: not bool(x)`, because it is not the output of the predicate function that is passed to `any`. In other words, it should be `g = (predicate(x) for x in seq)`.
Stephan202
Right you are. Fixed.
Robert Rossney
@cdleary, I guess you don't like zip(g,g) to group items from an iterator together then either
gnibbler
Okay, that's just disturbing.
Robert Rossney
+1  A: 

How about ...

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])
offby1
`functools.reduce(operator.__add__, ...)` is what sum is for!
cdleary
+1: for the `exactly_one()` name. `sum(1 for x in seq if x) == 1`
J.F. Sebastian
A: 

Here's modified @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)

Differences:

  1. predicate() is None by default. The meaning is the same as for built-in filter() and stdlib's itertools.ifilter() functions.

  2. More explicit function and parameters names (this is subjective).

  3. repeat() allows large n to be used.

Example:

if exactly_n_is_true(seq, 1, predicate):
   # predicate() is true for exactly one item from the seq
J.F. Sebastian
A: 

Look, Ma! No rtfm("itertools"), no dependency on predicate() returning a boolean, minimum evaluation, just works!

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
>>>
John Machin