范围作为 Python 中的字典键

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/39358092/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-19 22:10:35  来源:igfitidea点击:

Range as dictionary key in Python

pythondictionaryrange

提问by Cuylar Conly

So, I had an idea that I could use a range of numbers as a key for a single value in a dictionary.

所以,我有一个想法,我可以使用一系列数字作为字典中单个值的键。

I wrote the code bellow, but I cannot get it to work. Is it even possible?

我写了下面的代码,但我无法让它工作。甚至有可能吗?

    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]

回答by L3viathan

It is possible on Python 3 — and on Python 2 if you use xrangeinstead of range:

在 Python 3 上是可能的——如果你使用xrange而不是在 Python 2上range

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

However, the way you're trying to use it it won't work. You could iterate over the keys, like this:

但是,您尝试使用它的方式是行不通的。您可以遍历键,如下所示:

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

Performance of this isn't nice (O(n)) but if it's a small dictionary like you showed it's okay. If you actually want to do that, I'd subclass dictto work like that automatically:

这样做的性能并不好 (O(n)) 但如果它是一个像你展示的那样的小字典也没关系。如果你真的想这样做,我会dict自动子类化这样的工作:

class RangeDict(dict):
    def __getitem__(self, item):
        if not isinstance(item, range): # or xrange in Python 2
            for key in self:
                if item in key:
                    return self[key]
            raise KeyError(item)
        else:
            return super().__getitem__(item) # or super(RangeDict, self) for Python 2

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

回答by Jean-Fran?ois Fabre

Yes, you can, only if you convert your rangelists as immutable tuple, so they are hashable and accepted as keys of your dictionary:

是的,你可以,只有当你将你的range列表转换为 immutable tuple,所以它们是可散列的并被接受为字典的键时:

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

EDIT: actually it works in Python 3 as rangeis an immutable sequence type and generate an immutable tupleinstead of a listas L3viathan stated.

编辑:实际上它在 Python 3 中作为range不可变的序列类型工作,并生成不可变的tuple而不是listL3viathan 所说的。

but you cannot access them with a single integer as key though. Your last line won't work.

但是你不能用一个整数作为键来访问它们。你的最后一行行不通。

I took some time to create a solution which would work whatever the values may be (picking one entry in the dictionary works as long as the lines are not "weighted" by bigger ranges.

我花了一些时间来创建一个解决方案,无论值是什么,它都可以工作(只要行没有被更大的范围“加权”,就可以在字典中选择一个条目。

It calls bisecton the sorted keys to find the insertion point, hacks it a bit, and finds the best value in the dictionary, with O(log(N))complexity, which means it can handle a really big list (maybe a little too much here :) but the dictionary is also too much in that case)

它调用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]])

回答by Mark Ransom

You can't build a dictionary directly from a range, unless you want the range itself to be the key. I don't think you want that. To get individual entries for each possibility within the range:

您不能直接从范围构建字典,除非您希望范围本身成为键。我不认为你想要那个。要获取范围内每种可能性的单独条目:

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

When you have a dictindexed by a small range of integers, you really should consider using a listinstead:

当您有一个dict由小范围整数索引的索引时,您真的应该考虑使用 alist代替:

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.']

回答by Menglong Li

I wrote a RangeKeyDict class for handling cases like this, which is more general and easy to use. For usage, check the codes in __main__

我写了一个 RangeKeyDict 类来处理这样的情况,它更通用且易于使用。有关用法,请查看 __main__ 中的代码

to install it using:

使用以下方法安装它:

pip install range-key-dict

Usage:

用法:

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

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

回答by jpmc26

dictis the wrong tool for this job. dictis 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.

dict是这项工作的错误工具。dict用于将特定键映射到特定值。那不是你在做什么;您正在尝试映射范围。这里有一些更直接的选项。

Use ifblocks

使用if

For a small list of values, use the obvious and straightforward ifblocks:

对于一小部分值,请使用明显且直接的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))

There is absolutely nothing wrong with this approach. It really doesn't need to be any more complex. This is muchmore intuitive, much easier to figure out, and much more efficient than trying to use a dicthere.

这种方法绝对没有错。它真的不需要更复杂。这是很多更直观,比试图用一个更容易弄清楚,并更有效dict这里。

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.

这样做也使边界处理更加明显。在我上面提供的代码中,您可以快速发现范围是否使用<<=在每个地方使用。上面的代码还为 1 到 20 之外的值抛出有意义的错误消息。它也免费支持非整数输入,尽管您可能不关心这一点。

Map every value to an outcome

将每个值映射到结果

Instead of trying to use the ranges for the keys, you could reformulate your problem into one that doesmap specific keys to specific values. You do so by looping through the ranges and generating a full dictcontaining all the possible values:

而不是试图使用范围的钥匙,你可以重新制定您的问题变成一个映射特定键为特定值。您可以通过遍历范围并生成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))

In this case, we use the ranges to generate a dictthat we can look up a result in. We map each roll to an outcome, reusing the same outcomes multiple times. This is less straightforward; it's not quite as easy to discern the probability of each outcome from it. But at least it uses dictproperly: it maps a key to a value.

在这种情况下,我们使用范围来生成dict我们可以在其中查找结果的 a。我们将每个滚动映射到一个结果,多次重复使用相同的结果。这不那么简单;从中辨别每个结果的概率并不那么容易。但至少它使用dict得当:它将一个键映射到一个值。

Compute according to probabilities

根据概率计算

You couldchoose 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())
    
  • 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())
    

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. dictwas not designed for what you're doing.

“Pythonic”意味着让你的代码简单易懂。这意味着将结构用于其设计目的。dict不是为你正在做的事情而设计的。

Speed

速度

All of these options are comparatively fast. According to raratiru's comment, the RangeDictwas the fastest answer at the time. However, my testing scriptshows that except for numpy.random.choice, all the options I've suggested are about 40% to 50% faster:

所有这些选项都相对较快。根据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 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.

如果你一次得到一个结果,numpy 会慢一个数量级;但是,如果您批量生成结果,速度会快一个数量级。

回答by Paul Cornelius

This approach will accomplish what you want, and the last line will work (assumes Py3 behavior of rangeand print):

这种方法将完成您想要的,最后一行将起作用(假设 Py3 行为rangeprint):

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

BTW if you're simulating a 20-side die you need the final index to be 21, not 20 (since 20 is not in range(1,20)).

顺便说一句,如果您正在模拟 20 面骰子,则您需要最终索引为 21,而不是 20(因为 20 不在范围内(1,20))。

回答by TheLazyScripter

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]

回答by Terry Jan Reedy

The following is probably maximally efficient in mapping a randint to one of a set of fixed category strings with fixed probability.

以下可能是将 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]])

回答by Vasya Run

I might be late for the party, but here how I solved the similar problem.

我可能会迟到,但在这里我是如何解决类似问题的。

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

回答by Cuylar Conly

Thank you everyone for your responses. I kept hacking away, and I came up with a solution that will suit my purposes quite well. It is most similar to the suggestions of @PaulCornelius.

谢谢大家的回复。我一直在破解,我想出了一个非常适合我的目的的解决方案。它与@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.