简体   繁体   中英

Getting 1 number from 2 ranges with choice() and randint()

I have a block of code with

import random as r
value = r.choice(r.randint(10, 30), r.randint(50, 100))
print (value)

Why does it give me the following error, and how should I fix it?

TypeError: choice() takes 2 positional arguments but 3 were given 

choice takes a sequence of items to choose from, not varargs. Wrap the arguments to make an anonymous tuple or list and it'll work, changing:

value = r.choice(r.randint(10, 30), r.randint(50, 100))

to either of:

value = r.choice((r.randint(10, 30), r.randint(50, 100)))  # Tuple
value = r.choice([r.randint(10, 30), r.randint(50, 100)])  # List

random.choice accepts a single sequence. You have a couple of options as to how you generate the number you want.

To fix the immediate error, make the input a sequence:

value = r.choice([r.randint(10, 30), r.randint(50, 100)])

This is not a very good solution because it generates two numbers, which is wasteful. Furthermore, the resulting distribution is conditioned on the sizes of the ranges, which is likely incorrect.

A more general way to solve this is to generate a single number and map it to the range you want:

v = r.randint(0, 21 + 51)
value = v + 10 if v <= 20 else v - 21 + 50

If you want to specify the inputs as ranges and get a uniform distribution across all of them, you can generalize like this:

def from_ranges(*ranges):
    n = sum(map(len, ranges))
    v = random.randrange(n)
    for range in ranges:
        k = len(range)
        if v < k:
            return range[v]
        v -= k

This is nice because the ranges are small objects that don't take up a lot of space, but allow you a lot of flexibility in terms of what you can specify.

For the example in your question, you would call the function as

>>> from_ranges(range(10, 31), range(50, 101))

You could also eliminate the final linear search in favor of a binary search by accumulating the lengths into a cumulative sum:

from bisect import bisect
from itertools import accumulate
from random import randrange

def from_ranges(*ranges):
    lengths = list(accumulate(map(len, ranges)))
    v = randrange(lengths[-1])
    r = bisect(lengths, v)
    offset = lengths[r - 1] if r > 0 else 0
    return ranges[r][v - offset]

This solution would be faster if you were to memoize the accumulation, before the random number is generated. Otherwise, it offers little advantage over the first solution.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM