[Python-ideas] random.choice on non-sequence

Chris Angelico rosuav at gmail.com
Tue Apr 12 22:02:43 EDT 2016


On Wed, Apr 13, 2016 at 7:40 AM, Rob Cliffe <rob.cliffe at btinternet.com> wrote:
> And here is my proposed (tested) version:
>
>     def choice(self, it):
>         """Choose a random element from a non-empty iterable."""
>         try:
>             it[0]
>         except IndexError:
>             raise IndexError('Cannot choose from an empty sequence')
>         except TypeError:
>             it = tuple(it)
>         try:
>             i = self._randbelow(len(it))
>         except ValueError:
>             raise IndexError('Cannot choose from an empty iterable')
>         return it[i]
>
>
> This works on (e.g.) a list/tuple/string, a set, a dictionary view or a
> generator.
> Obviously the generator has to be 'fresh' (i.e. any previously consumed
> values will be excluded from the choice) and 'throw-away' (random.choice
> will exhaust it).
> But it means you can write code like
>     random.choice(x for x in xrange(10) if x%3)    # this feels quite
> "Pythonic" to me!

Small point of order: Pretty much everything discussed here on
python-ideas is about Python 3. It's best to make sure your code works
with the latest CPython (currently 3.5), as a change like this would
be landing in 3.6 at the earliest. So what I'd be looking at is this:

random.choice(x for x in range(10) if x%3)

AFAIK this doesn't change your point at all, but it is worth being
careful of; Python 3's range object isn't quite the same as Python 2's
xrange, and it's possible something might "just work". (For the
inverse case, "if x%3 == 0", you can simply use
random.choice(random(0, 10, 3)) to do what you want.)

I don't like the proposed acceptance of arbitrary iterables. In the
extremely rare case where you actually do want that, you can always
manually wrap it in list() or tuple(). But your original use-case does
have merit:

> It surprised me a bit the first time I realised that random.choice did not work on a set.

A set has a length, but it can't be indexed. It should be perfectly
reasonable to ask for a random element out of a set! So here's my
counter-proposal: Make random.choice accept any iterable with a
__len__.

    def choice(self, coll):
        """Choose a random element from a non-empty collection."""
        try:
            i = self._randbelow(len(coll))
        except ValueError:
            raise IndexError('Cannot choose from an empty collection')
        try:
            return coll[i]
        except TypeError:
            for _, value in zip(range(i+1), coll):
                pass
            return value

Feel free to bikeshed the method of iterating part way into a
collection (enumerate() and a comparison? call iter, then call next
that many times, then return next(it)?), but the basic concept is that
you have to have a length and then you iterate into it that far.

It's still not arbitrary iterables, but it'll handle sets and dict
views. Handling dicts directly may be a little tricky; since they
support subscripting, they'll either raise KeyError or silently return
a value, where the iteration-based return value would be a key.
Breaking this out into its own function would be reliable there (take
out the try/except and just go straight into iteration).

ChrisA


More information about the Python-ideas mailing list