dict
is the wrong tool for this job. dict
is for mapping specific keys to specific values. That isn't what you're doing; you're trying to map ranges. Here are some more straightforward options.
Map every value to an outcome
Instead of trying to use the ranges for the keys, you could reformulate your problem into one that does map specific keys to specific values. You do so by looping through the ranges and generating a full dict
containing all the possible values:
def setall(d, keys, value):
for k in keys:
d[k] = value
OUTCOMES = {}
setall(OUTCOMES, range(1, 6), 'You are about as stealthy as thunderstorm.')
setall(OUTCOMES, range(6, 11), 'You tip-toe through the crowd of walkers, while loudly calling them names.')
setall(OUTCOMES, range(11, 16), 'You are quiet, and deliberate, but still you smell.')
setall(OUTCOMES, range(16, 21), 'You move like a ninja, but attracting a handful of walkers was inevitable.')
def get_stealthiness(roll):
if roll not in OUTCOMES.keys():
raise ValueError('Unsupported roll: {}'.format(roll))
return OUTCOMES[roll]
stealth_roll = randint(1, 20)
print(get_stealthiness(stealth_roll))
In this case, we use the ranges to generate a dict
that we can look up a result in. We map each roll to an outcome, reusing the same outcomes multiple times. This uses dict
properly: it maps a single key to a single value.
Use if
blocks
For a small list of values, using the obvious and straightforward if
blocks is perfectly fine:
def get_stealthiness(roll):
if 1 <= roll < 6:
return 'You are about as stealthy as thunderstorm.'
elif 6 <= roll < 11:
return 'You tip-toe through the crowd of walkers, while loudly calling them names.'
elif 11 <= roll < 16:
return 'You are quiet, and deliberate, but still you smell.'
elif 16 <= roll <= 20:
return 'You move like a ninja, but attracting a handful of walkers was inevitable.'
else:
raise ValueError('Unsupported roll: {}'.format(roll))
stealth_roll = randint(1, 20)
print(get_stealthiness(stealth_roll))
There is absolutely nothing wrong with this approach. It really doesn't need to be any more complex. This is much more intuitive, much easier to figure out, and much more efficient than trying to use a dict
with range
s as keys.
Doing it this way also makes the boundary handling more visible. In the code I present above, you can quickly spot whether the range uses <
or <=
in each place. The code above also throws a meaningful error message for values outside of 1 to 20. It also supports non-integer input for free, although you may not care about that.
Compute according to probabilities
You could choose the result based on a probabilities calculation. The basic idea is to compute a "cumulative" probability (which you already have with the top end of the roll values) and then loop through until the cumulative probability exceeds the random value. There's plenty of ideas of how to go about it here.
Some simple options are:
numpy.random.choice
A loop:
# Must be in order of cummulative weight
OUTCOME_WITH_CUM_WEIGHT = [
('You are about as stealthy as thunderstorm.', 5),
('You tip-toe through the crowd of walkers, while loudly calling them names.', 10),
('You are quiet, and deliberate, but still you smell.', 15),
('You move like a ninja, but attracting a handful of walkers was inevitable.', 20),
]
def get_stealthiness(roll):
if 1 > roll or 20 < roll:
raise ValueError('Unsupported roll: {}'.format(roll))
for stealthiness, cumweight in OUTCOME_WITH_CUM_WEIGHT:
if roll <= cumweight:
return stealthiness
raise Exception('Reached end of get_stealthiness without returning. This is a bug. roll was ' + str(roll))
stealth_roll = randint(1, 20)
print(get_stealthiness(stealth_roll))
random.choices
(requires Python 3.6 or higher)
OUTCOMES_SENTENCES = [
'You are about as stealthy as thunderstorm.',
'You tip-toe through the crowd of walkers, while loudly calling them names.',
'You are quiet, and deliberate, but still you smell.',
'You move like a ninja, but attracting a handful of walkers was inevitable.',
]
OUTCOME_CUMULATIVE_WEIGHTS = [5, 10, 15, 20]
def make_stealth_roll():
return random.choices(
population=OUTCOMES_SENTENCES,
cum_weights=OUTCOME_CUMULATIVE_WEIGHTS,
)
print(make_stealth_roll())
Some have the downside of taking the actual numeric roll out of your hands, but they're a lot simpler to implement and maintain.
Pythonic
"Pythonic" means keeping your code straightforward and approachable. It means using structures for the purposes they were designed for. dict
was not designed for what you're doing.
Speed
All of these options are comparatively fast. According to raratiru's comment, the RangeDict
was the fastest answer at the time. However, my testing script shows that except for numpy.random.choice
, all the options I've suggested are about 30% to 45% faster:
get_stealthiness_rangedict(randint(1, 20)): 2.347477641014848 µs per loop (baseline)
get_stealthiness_ifs(randint(1, 20)): 1.3355229599983431 µs per loop (56.89% of baseline)
get_stealthiness_dict(randint(1, 20)): 1.3621300339582376 µs per loop (58.03% of baseline)
get_stealthiness_cumweight(randint(1, 20)): 1.4149694619700313 µs per loop (60.28% of baseline)
make_stealth_roll_randomchoice(): 1.6616826370009221 µs per loop (70.79% of baseline)
make_stealth_roll_numpychoice(): 27.418932934000622 µs per loop (1168.02% of baseline)
numpy.choice all at once: 0.5978886220254935 µs per loop (25.47% of baseline)
numpy is an order of magnitude slower if you get one result at a time from it; however, it's an order of magnitude faster if you generate your results in bulk.