Question

I created a utility function to return the expected single item from an generator expression

print one(name for name in ('bob','fred') if name=='bob')

Is this a good way to go about it?

def one(g):
    try:
        val = g.next()
        try:
            g.next()
        except StopIteration:
            return val
        else:
            raise Exception('Too many values')
    except StopIteration:
        raise Exception('No values')
Was it helpful?

Solution

A simpler solution is to use tuple unpacking. This will already do everything you want, including checking that it contains exactly one item.

Single item:

 >>> name, = (name for name in ('bob','fred') if name=='bob')
 >>> name
 'bob'

Too many items:

>>> name, = (name for name in ('bob','bob') if name=='bob')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack

No items:

>>> name, = (name for name in ('fred','joe') if name=='bob')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: need more than 0 values to unpack

OTHER TIPS

Simple approach:

print (name for name in ('bob', 'fred') if name == 'bob').next()

If you really want an error when there is more than one value, then you need a function. The most simple I can think of is (EDITED to work with lists too):

def one(iterable):
    it = iter(iterable)
    val = it.next()
    try:
        it.next()
    except StopIteration:
        return val
    else:
        raise Exception('More than one value')

For those using or interested in a third-party library, more_itertools implements such a tool with native error handling:

> pip install more_itertools

Code

import more_itertools as mit


mit.one(name for name in ("bob", "fred") if name == "bob")
# 'bob'

mit.one(name for name in ("bob", "fred", "bob") if name == "bob")
# ValueError: ...

mit.one(name for name in () if name == "bob")
# ValueError: ...

See more_itertools docs for details. The underlying source code is similar to the accepted answer.

Have a look into the itertools.islice() method.

>>> i2=itertools.islice((name for name in ('bob','fred') if name=='bob'),0,1,1)
>>> i2.next()
'bob'
>>> i2.next()
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
StopIteration
>>> 

This module implements a number of iterator building blocks inspired by constructs from the Haskell and SML programming languages. Each has been recast in a form suitable for Python.

The module standardizes a core set of fast, memory efficient tools that are useful by themselves or in combination. Standardization helps avoid the readability and reliability problems which arise when many different individuals create their own slightly varying implementations, each with their own quirks and naming conventions.

The tools are designed to combine readily with one another. This makes it easy to construct more specialized tools succinctly and efficiently in pure Python.

Do you mean?

def one( someGenerator ):
    if len(list(someGenerator)) != 1: raise Exception( "Not a Singleton" )

What are you trying to accomplish with all the extra code?

Here is my try at the one() function. I would avoid the explicit .next() call and use a for loop instead.

def one(seq):
    counter = 0
    for elem in seq:
        result = elem
        counter += 1
        if counter > 1:
            break
    if counter == 0:
        raise Exception('No values')
    elif counter > 1:
        raise Exception('Too many values')
    return result

First, (to answer the actual question!) your solution will work fine as will the other variants proposed.

I would add that in this case, IMO, generators are overly complicated. If you expect to have one value, you'll probably never have enough for memory usage to be a concern, so I would have just used the obvious and much clearer:

children = [name for name in ('bob','fred') if name=='bob']
if len(children) == 0:
    raise Exception('No values')
elif len(children) > 1:
    raise Exception('Too many values')
else:
    child = children[0]

How about using Python's for .. in syntax with a counter? Similar to unbeknown's answer.

def one(items):
    count = 0
    value = None

    for item in items:
        if count:
            raise Exception('Too many values')

        count += 1
        value = item

    if not count:
        raise Exception('No values')

    return value
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top