Currently, random.choice requires a sequence:
random.choice({1, 2, 3}) # TypeError: 'set' object is not subscriptable
Conversion of the argument to list or tuple works:
random.choice(list({1, 2, 3})) # Okay
This is a bit of an inconvenience (I forget, see the error, and correct; wasted another 2 min ;-), and is wasteful for large collections.
A Sized Iterable parameter would suffice, a.k.a. a Collection (with Container):
def choice2[T](self, col: Collection[T]) -> T:
"""Choose a random element from a non-empty sized iterable."""
# As an accommodation for NumPy, we don't use "if not col"
# because bool(numpy.array()) raises a ValueError.
if not len(col):
raise IndexError('Cannot choose from an empty collection')
# To allow e.g. set next to sequence, use islice instead of subscripting
return next(itertools.islice(col, self._randbelow(len(col)), None))
choice2({1, 2, 3}) # Okay
The downside is that islice partially consumes the iterable, and therefore is slower than subscripting for sequences.
To prevent loss of performance on large sequences, and still gain performance on large sets, plus gain convenience of a more versatile choice, we need to try both:
def choice3[T](self, col: Collection[T]) -> T:
"""Choose a random element from a non-empty sized iterable."""
# Avoiding to call len() multiple times, shaves ~4% from choice's runtime
l = len(col)
# As an accommodation for NumPy, we don't use "if not col"
# because bool(numpy.array()) raises a ValueError.
if not l:
raise IndexError('Cannot choose from an empty collection')
# By subscripting or islice, we need the same element number
n = self._randbelow(l)
try:
# To keep the speed for sequence, use subscripting
return col[n]
except TypeError:
# To allow e.g. set, use islice instead
return next(itertools.islice(col, n, None))
choice3([1, 2, 3]) # Okay (and still fast for large sequences)
choice3({1, 2, 3}) # Okay (and faster than conversion to list for large sets)