Диапазон в виде словарного ключа в Python

Итак, у меня возникла идея, что я могу использовать диапазон чисел в качестве ключа для одного значения в словаре.

Я написал код ниже, но я не могу заставить его работать. Возможно ли это?

    stealth_roll = randint(1, 20)
    # select from a dictionary of 4 responses using one of four ranges.
    ## not working.
    stealth_check = {
                    range(1, 6) : 'You are about as stealthy as thunderstorm.',
                    range(6, 11) : 'You tip-toe through the crowd of walkers, while loudly calling them names.',
                    range(11, 16) : 'You are quiet, and deliberate, but still you smell.',
                    range(16, 20) : 'You move like a ninja, but attracting a handful of walkers was inevitable.'
                    }

    print stealth_check[stealth_roll]

Ответ 1

Это возможно на Python 3 - и на Python 2, если вы используете xrange вместо range:

stealth_check = {
                xrange(1, 6) : 'You are about as stealthy as thunderstorm.', #...
                }

Однако, как вы пытаетесь его использовать, это не сработает. Вы можете перебирать ключи, например:

for key in stealth_check:
    if stealth_roll in key:
        print stealth_check[key]
        break

Выполнение этого не очень хорошо (O (n)), но если это небольшой словарь, как вы показали его в порядке. Если вы действительно хотите это сделать, я бы подклассом dict работал так автоматически:

class RangeDict(dict):
    def __getitem__(self, item):
        if type(item) != range: # or xrange in Python 2
            for key in self:
                if item in key:
                    return self[key]
        else:
            return super().__getitem__(item)

stealth_check = RangeDict({range(1,6): 'thunderstorm', range(6,11): 'tip-toe'})
stealth_roll = 8
print(stealth_check[stealth_roll]) # prints 'tip-toe'

Ответ 2

Вы не можете создавать словарь непосредственно из диапазона, если вы не хотите, чтобы сам диапазон был ключом. Я не думаю, что ты этого хочешь. Чтобы получить отдельные записи для каждой возможности в пределах диапазона:

stealth_check = dict(
                    [(n, 'You are about as stealthy as thunderstorm.')
                        for n in range(1, 6)] +
                    [(n, 'You tip-toe through the crowd of walkers, while loudly calling them names.')
                        for n in range(6, 11)] +
                    [(n, 'You are quiet, and deliberate, but still you smell.')
                        for n in range(11, 16)] +
                    [(n, 'You move like a ninja, but attracting a handful of walkers was inevitable.')
                        for n in range(16, 20)]
                    )

Если у вас есть dict, проиндексированный небольшим диапазоном целых чисел, вам действительно стоит использовать вместо list:

stealth_check = [None]
stealth_check[1:6] = (6 - 1) * ['You are about as stealthy as thunderstorm.']
stealth_check[6:11] = (11 - 6) * ['You tip-toe through the crowd of walkers, while loudly calling them names.']
stealth_check[11:16] = (16 - 11) * ['You are quiet, and deliberate, but still you smell.']
stealth_check[16:20] = (20 - 16) * ['You move like a ninja, but attracting a handful of walkers was inevitable.']

Ответ 3

Да, вы можете, только если вы конвертируете свои списки range в качестве неизменяемых tuple, поэтому они хешируются и принимаются как ключи вашего словаря:

stealth_check = {
                tuple(range(1, 6)) : 'You are about as stealthy as thunderstorm.',

EDIT: на самом деле он работает в Python 3, поскольку range является неизменным типом последовательности и генерирует неизменяемый tuple вместо list, как указано в L3viathan.

но вы не можете получить к ним доступ только с одним целым числом. Ваша последняя строка не будет работать.

Мне потребовалось некоторое время, чтобы создать решение, которое будет работать независимо от значений (выбор одной записи в словаре работает до тех пор, пока строки не будут "взвешены" большими диапазонами.

Он вызывает bisect на отсортированных клавишах, чтобы найти точку вставки, немного взломает его и находит лучшее значение в словаре, с O(log(N)) сложностью, что означает, что он может обрабатывать действительно большой список (возможно, немного слишком много здесь:), но словарь тоже слишком много в этом случае)

from random import randint
import bisect

stealth_roll = randint(1, 20)
# select from a dictionary of 4 responses using one of four thresholds.

stealth_check = {
                1 : 'You are about as stealthy as thunderstorm.',
                6 : 'You tip-toe through the crowd of walkers, while loudly calling them names.',
                11 : 'You are quiet, and deliberate, but still you smell.',
                16 : 'You move like a ninja, but attracting a handful of walkers was inevitable.'
                }

sorted_keys = sorted(stealth_check.keys())


insertion_point = bisect.bisect_left(sorted_keys,stealth_roll)

# adjust, as bisect returns not exactly what we want
if insertion_point==len(sorted_keys) or sorted_keys[insertion_point]!=stealth_roll:
    insertion_point-=1

print(insertion_point,stealth_roll,stealth_check[sorted_keys[insertion_point]])

Ответ 4

Я написал класс RangeKeyDict для обработки таких случаев, который является более общим и простым в использовании. Для использования проверьте коды в __main __

чтобы установить его, используя:

pip install range-key-dict

Использование:

from range_key_dict import RangeKeyDict

if __name__ == '__main__':
    range_key_dict = RangeKeyDict({
        (0, 100): 'A',
        (100, 200): 'B',
        (200, 300): 'C',
    })

    # test normal case
    assert range_key_dict[70] == 'A'
    assert range_key_dict[170] == 'B'
    assert range_key_dict[270] == 'C'

    # test case when the number is float
    assert range_key_dict[70.5] == 'A'

    # test case not in the range, with default value
    assert range_key_dict.get(1000, 'D') == 'D'

https://github.com/albertmenglongli/range-key-dict

Ответ 5

dict не тот инструмент для этой работы. dict предназначен для отображения определенных ключей на определенные значения. Это не то, что вы делаете; Вы пытаетесь отобразить диапазоны. Вот еще несколько простых вариантов.

Используйте блоки if

Для небольшого списка значений используйте очевидные и простые блоки if:

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))

В этом подходе нет ничего плохого. Это действительно не должно быть более сложным. Это гораздо более интуитивно понятно, гораздо проще понять и гораздо эффективнее, чем пытаться использовать dict здесь.

Это также делает обработку границ более заметной. В приведенном выше коде вы можете быстро определить, использует ли диапазон < или <= в каждом месте. Приведенный выше код также выдает значимое сообщение об ошибке для значений от 1 до 20. Он также бесплатно поддерживает нецелочисленный ввод, хотя вы можете не заботиться об этом.

Сопоставьте каждое значение с результатом

Вместо того, чтобы пытаться использовать диапазоны для ключей, вы можете переформулировать свою проблему в проблему, которая сопоставляет конкретные ключи с конкретными значениями. Вы делаете это, просматривая диапазоны и генерируя полный dict, содержащий все возможные значения:

OUTCOMES = {}
for i in range(1, 6):
    OUTCOMES[i] = 'You are about as stealthy as thunderstorm.'
for i in range(6, 11):
    OUTCOMES[i] = 'You tip-toe through the crowd of walkers, while loudly calling them names.'
for i in range(11, 16):
    OUTCOMES[i] = 'You are quiet, and deliberate, but still you smell.'
for i in range(16, 21):
    OUTCOMES[i] = '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))

В этом случае мы используем диапазоны для генерации dict, в котором мы можем искать результат. Мы сопоставляем каждый бросок с результатом, многократно используя одни и те же результаты. Это менее просто; не так легко определить вероятность каждого исхода из этого. Но, по крайней мере, он правильно использует dict: он сопоставляет ключ со значением.

Вычислить в соответствии с вероятностями

Вы можете выбрать результат на основе расчета вероятностей. Основная идея состоит в том, чтобы вычислить "кумулятивную" вероятность (которая у вас уже есть с верхним пределом значений броска), а затем выполнить цикл, пока кумулятивная вероятность не превысит случайное значение. Там много идей о том, как это сделать здесь.

Вот несколько простых вариантов:

  • numpy.random.choice
  • Цикл:

    # 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 (требуется Python 3.6 или выше)

    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())
    

У некоторых есть и обратная сторона - вынимать из рук фактический числовой результат, но их гораздо проще реализовать и поддерживать.

Pythonic

"Pythonic" означает, что ваш код должен быть простым и доступным. Это означает использование конструкций для целей, для которых они были предназначены. dict не был предназначен для того, что вы делаете.

Скорость

Все эти варианты сравнительно быстрые. Согласно raratiruкомментарию, RangeDict был самым быстрым ответом в то время. Однако мой сценарий тестирования показывает, что, кроме numpy.random.choice, все предложенные варианты работают примерно на 40-50% быстрее:

get_stealthiness_rangedict(randint(1, 20)): 3.4458323369617574 µs per loop
get_stealthiness_ifs(randint(1, 20)): 1.8013543629786 µs per loop
get_stealthiness_dict(randint(1, 20)): 1.9512669100076891 µs per loop
get_stealthiness_cumweight(randint(1, 20)): 1.9908560069743544 µs per loop
make_stealth_roll_randomchoice(): 2.037966169009451 µs per loop
make_stealth_roll_numpychoice(): 38.046008297998924 µs per loop
numpy.choice all at once: 0.5016623589908704 µs per loop

numpy на порядок медленнее, если вы получаете по одному результату за раз; тем не менее, это на порядок быстрее, если вы генерируете свои результаты оптом.

Ответ 6

stealth_check = {
                    0 : 'You are about as stealthy as thunderstorm.',
                    1 : 'You tip-toe through the crowd of walkers, while loudly calling them names.',
                    2 : 'You are quiet, and deliberate, but still you smell.',
                    3 : 'You move like a ninja, but attracting a handful of walkers was inevitable.'
                    }
stealth_roll = randint(0, len(stealth_check))
return stealth_check[stealth_roll]

Ответ 7

Этот подход выполнит то, что вы хотите, и последняя строка будет работать (предполагает поведение Py3 range и print):

def extend_dict(d, value, x):
    for a in x:
        d[a] = value

stealth_roll = randint(1, 20)
# select from a dictionary of 4 responses using one of four ranges.
## not working.
stealth_check = {}
extend_dict(stealth_check,'You are about as stealthy as thunderstorm.',range(1,6))
extend_dict(stealth_check,'You tip-toe through the crowd of walkers, while loudly calling them names.',range(6,11))
extend_dict(stealth_check,'You are quiet, and deliberate, but still you smell.',range(11,16))
extend_dict(stealth_check,'You move like a ninja, but attracting a handful of walkers was inevitable.',range(16,20))

print(stealth_check[stealth_roll])

Кстати, если вы имитируете 20-стороннюю матрицу, вам нужен конечный индекс 21, а не 20 (поскольку 20 не находится в диапазоне (1,20)).

Ответ 8

Вероятно, максимально вероятно, что при преобразовании randint в один из наборов строк с фиксированной категорией с фиксированной вероятностью может быть максимально эффективным.

from random import randint
stealth_map = (None, 0,0,0,0,0,0,1,1,1,1,1,2,2,2,2,2,3,3,3,3)
stealth_type = (
    '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.',
    )
for i in range(10):
    stealth_roll = randint(1, 20)
    print(stealth_type[stealth_map[stealth_roll]])

Ответ 9

Я мог бы опоздать на вечеринку, но вот как я решил подобную проблему.

import bisect

outcomes = ["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."]
ranges = [6, 11, 16]

outcome_index = bisect.bisect(ranges, 20)
print(outcomes[outcome_index])

Ответ 10

Спасибо всем за ваши ответы. Я продолжал взламывать, и я придумал решение, которое подойдет для моих целей достаточно хорошо. Это наиболее похоже на предложения @PaulCornelius.

stealth_roll = randint(1, 20)
# select from a dictionary of 4 responses using one of four ranges.
# only one resolution can be True. # True can be a key value.

def check(i, a, b): # check if i is in the range. # return True or False
    if i in range(a, b):
        return True
    else:
        return False
### can assign returned object as dictionary key! # assign key as True or False.
stealth_check = {
                check(stealth_roll, 1, 6) : 
                'You are about as stealthy as a thunderstorm.',
                check(stealth_roll, 6, 11) : 
                'You tip-toe through the crowd of walkers, while loudly calling them names.',
                check(stealth_roll, 11, 16) : 
                'You are quiet, and deliberate, but still you smell.',
                check(stealth_roll, 15, 21) : 
                'You move like a ninja, but attracting a handful of walkers was inevitable.'
                }

print stealth_check[True] # print the dictionary value that is True.