【问题标题】:Range as dictionary key in Python范围作为Python中的字典键
【发布时间】:2016-09-06 21:25:26
【问题描述】:

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

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

    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]

【问题讨论】:

  • 与其尝试使用范围作为键,为什么不根据字典的大小滚动?
  • 作为旁注,这样的字典在 python3 上是可能的。由于键是范围,因此您必须相应地访问字典:stealth_check[range(6, 11)] 将起作用。不过,这对您的目的完全没有用,只是想表明对象模型是一致的。
  • @TheLazyScripter 我在整个脚本中都采用了 (1, 20) 约定,我通常还使用随机值作为乘数以及场景选择器。此外,如果这可行,我可以对每个可能的结果应用不同的权重。
  • 我一直在问这个问题,但从来没有得到很好的答案。我发现简单地使用 if/elif/else 结构来完成它会更好。

标签: python dictionary range


【解决方案1】:

如果您使用xrange 而不是range,则可以在 Python 3 和 Python 2 上实现:

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

【讨论】:

  • 使用 ipython timeit 测量 RangeDict 对这些数据的执行时间,证明它是迄今为止提到的最快的技术:Best of 3: 6.47 µs per loop, the slowest run took 6.15 times longer than the fastest 而最好的其他技术返回的数字如Best of 3: 17 µs per loop, the slowest run took 20 times longer than the fastest
  • 请注意,您已经修改了dict 的行为。如果找不到非range 键,它不再抛出错误。我会在循环之后添加一个raise
  • @raratiru 我已经为我的答案添加了一些额外的替代方案。除了 numpy 之外,通过我在 Python 3.6 中的测试,它们都超过了这个。它们以大约 50% 到 60% 的速度进入。
  • if type(item) != range 不应该是 if type(item) == range
  • @baklarz2048 不,因为“特殊”用法是当您尝试使用正常数字对其进行索引时,例如stealth_check[8]。只有这样我们才想做我们的特殊处理。
【解决方案2】:

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 更直观、更容易理解、更高效。

这样做也使边界处理更加明显。在我上面提供的代码中,您可以快速发现范围是否在每个位置使用&lt;&lt;=。上面的代码还会为 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:它将一个键映射到一个值。

根据概率计算

可以根据概率计算选择结果。基本思想是计算“累积”概率(您已经拥有滚动值的顶端),然后循环直到累积概率超过随机值。有很多关于如何去做的想法here

一些简单的选项是:

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

有些方法的缺点是无法控制实际的数字滚动,但它们的实现和维护要简单得多。

Python 风格

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

速度

所有这些选项都比较快。根据raratirucommentRangeDict是当时最快的答案。但是,我的testing script 表明,除了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 会慢一个数量级;但是,如果您批量生成结果,速度会快一个数量级。

【讨论】:

  • 我认为这是回答更深层次问题而不是表面问题的一个很好的例子。我理解为什么有些人会反对(因为它有点说“错误”),但很高兴知道有人知道如何为工作选择正确的工具。你会得到我的 +1
【解决方案3】:

是的,您可以,但前提是您将 range 列表转换为不可变的 tuple,因此它们是可散列的并被接受为字典的键:

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

编辑:实际上它在 Python 3 中工作,因为 range 是一个不可变的序列类型,并生成一个不可变的 tuple 而不是 L3viathan 所述的 list

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

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

它在排序后的键上调用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)


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】:

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

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

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

        【解决方案6】:

        我写了一个 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

        【讨论】:

        • 问题的时间复杂度是 O(log(N)),而你的算法的时间复杂度是 O(N)。换句话说,您的解决方案的扩展速度可能不是最理想的。
        【解决方案7】:

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

        【讨论】:

        • 不错的解决方案。但是,我使用timeit 来测量每种技术执行所需的时间,并且 - 到目前为止 - 最有效的是subclasses dict
        【解决方案8】:
        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]
        

        【讨论】:

        • 是的。不过,只有在概率平衡时才有效。
        • ……虽然我根本不明白使用字典的意义。 python 中存在索引的、基于 0 的连续集合,它们被称为列表:p
        【解决方案9】:

        这种方法将完成您想要的,最后一行将起作用(假设 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])
        

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

        【讨论】:

        • randint 返回 1 到 20 之间的值。最后一行应该是 range(16,21)。这是你的意思吗?
        • 是的,random.randint(1,20) 返回一个从 1 到 20 的值,包括 1 到 20;但 range(1,20) 仅从 1 到 19 步长,包括在内。所以他的代码会运行,但几率不会是他所期望的。我知道这一点,因为我自己不止一次犯过这个错误:-)
        【解决方案10】:

        在将随机数映射到具有固定概率的一组固定类别字符串中的一个时,以下可能是最有效的。

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

        【讨论】:

          猜你喜欢
          • 2019-11-03
          • 2017-03-23
          • 2011-01-09
          • 1970-01-01
          • 2012-11-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-05-20
          相关资源
          最近更新 更多