【问题标题】:efficiency of long (str) keys in python dictionarypython字典中长(str)键的效率
【发布时间】:2015-03-24 20:40:48
【问题描述】:

我正在解析一些 xml(带有一些 python 3.4 代码)并且想要从节点及其 id 属性中检索文本。例子: <li id="12345"> Some text here </li> 我当前的代码仅围绕文本构建(我现在添加了 id,但以前不需要它)。我正在遍历文本/句子列表,然后继续做一些事情。所以我想制作一个以文本/句子为键,以这个id属性为值的字典。

但是,这感觉不是很有效。文本可以是一个完整的段落,使键很长。而 id 的长度总是相当有限(但仍然是 str 类型,例如一些字母字符后跟一些数字)。 但是让 ids 成为 key 和 text 成为 value 需要对代码进行一些重写。一切都不是很成问题,但这只是让我想知道:与像“ulp_887362487687678”这样的 id 作为键相比,将文本(可能是整个段落)作为键有多低效?

我可以制作两个反向字典(一个以 id 为键,另一个以文本为键)并比较构造和查找等等。而且我还发现了一些关于密钥长度限制的主题 (Do Dictionaries have a key length limit?)。但我只是想知道你对此有何看法。在你的 dict 中有这么长的 str 键是你绝对想要避免的,还是不是什么大不了的事? 如果你能分享一些赞成/反对的意见,那就太好了!

【问题讨论】:

    标签: python dictionary key


    【解决方案1】:

    Python 字符串长度会对字典性能产生非常显着的影响,但您必须使用非常大的字符串。

    问题显然是,一旦您找到了正确的哈希存储桶,您仍然需要进行字符串比较以查看是否匹配。对两个大字符串进行字符串比较是昂贵的,除非它们在内存中是相同的字符串对象(在这种情况下,Python 足够聪明,可以轻松地声明相等)。

    >>> import timeit
    >>> for i in range(7):
    ...   dkey = "X" * (10**i)
    ...   skey = "X" * (10**i)    # Different object; no fast path
    ...   d = {dkey: 1}
    ...   tmp = d[skey]           # Hash is now cached on skey object
    ...   timing = timeit.timeit('d[skey] == 1', globals=globals(), number=1000)
    ...   print(len(dkey), " timing is ", timing*1000, " microseconds")
    ... 
    1  timing is  0.119031872600317  microseconds
    10  timing is  0.1442211214452982  microseconds
    100  timing is  0.1361379399895668  microseconds
    1000  timing is  0.16252091154456139  microseconds
    10000  timing is  0.5145659670233727  microseconds
    100000  timing is  5.568335996940732  microseconds
    1000000  timing is  63.68113495409489  microseconds
    >>> 
    

    长度可达 1000 左右的字符串,由于字符串大小,开销很小。当您的长度超过 10000 时,就字符串长度而言,字典查找似乎实际上是 O(N)。

    【讨论】:

      【解决方案2】:

      hash() 对字符串的性能确实是O(n)但是结果被缓存在字符串中——重复调用会使用缓存的值。这是可能的,因为字符串是不可变的。 Martijn 的代码使用了timeit重复 特性,所以你看不到这种效果,因为在最后一种情况下,10000010 次中有 10000009 次没有计算哈希码。

      下面还是O(n):

      import random
      from timeit import timeit
      
      for i in range(10):
          length = 10 ** i
          # notice number=1 !!!
          timing = timeit('hash(t)', 't = "a" * {}'.format(length), number=1)
          print('Length: {:10d}, timing: {:.20f}'.format(length, timing))
      
      Length:          1, timing: 0.00000437500057159923
      Length:         10, timing: 0.00000287900184048340
      Length:        100, timing: 0.00000342299972544424
      Length:       1000, timing: 0.00000459299917565659
      Length:      10000, timing: 0.00002153400055249222
      Length:     100000, timing: 0.00006719700104440562
      Length:    1000000, timing: 0.00066680999952950515
      Length:   10000000, timing: 0.00673243699930026196
      Length:  100000000, timing: 0.04393487600100343116
      Length: 1000000000, timing: 0.39340837700001429766
      

      差异是由于时间错误、分支预测等原因造成的。

      【讨论】:

      • 这不包括解释和编译包含长字符串的代码的时间吗?
      • @user48956 不,该字符串是在设置代码中创建的,不包含在计时中。此外,代码不包含长字符串,而是设置代码的格式为t = "a" * n
      【解决方案3】:

      在大多数情况下,@Martijn Pieters 的回答是正确的,也就是说,理论上是正确的。 但是,实际上,在性能方面您需要考虑很多事情。

      我最近遇到了将长字符串作为键散列的问题,我在实践中遇到了超时错误,只是因为python的字典键散列。我知道这一点,因为我使用 JavaScript 对象作为“字典”解决了这个问题,它工作得很好,这意味着没有超时错误。

      然后由于我的键实际上是一长串数字列表,我将它们改为数字元组(不可变对象可以是键)。这也很有效。

      话虽如此,我使用散列函数@Martijn Pieters 在示例中使用一长串数字作为键作为元组版本的键来测试时间元组版本在他们的 python 编译器 repl.it 上需要更长的时间。我不是在谈论 0.1 的差异。这是 0.02 和 12.02 之间的差异

      是不是很奇怪?! :>

      现在的重点是,每个环境都各不相同。您的操作量会累积。因此,您不能简单地说某些操作需要更长或更短。即使是 0.01 秒的操作,只执行 1000 次,也会让用户等待 10 秒。

      对于任何生产环境,如果需要,您真的想尝试优化您的算法,并始终使用更好的设计。对于普通软件,它可以节省用户的宝贵时间。对于云服务,我们谈论的是美元钞票。

      最后,我绝对不要推荐使用长字符串作为键,因为我在不同的环境中得到的结果不一致。如果需要,您肯定希望使用 id 作为键并遍历字符串值以查找 id。但是如果你必须使用长字符串作为键,考虑限制对字典的操作数。保留两个版本绝对是浪费空间/RAM。性能和记忆的主题是另一课。

      【讨论】:

        【解决方案4】:

        不,Python 字符串长度对字典性能几乎没有影响。字符串长度可能产生的唯一影响是 hash() 函数用于将键映射到哈希表槽。

        字符串长度对hash()的性能影响很小:

        >>> import random
        >>> from timeit import timeit
        >>> from string import ascii_letters
        >>> generate_text = lambda len: ''.join([random.choice(ascii_letters) for _ in xrange(len)])
        >>> for i in range(8):
        ...     length = 10 + 10 ** i
        ...     testword = generate_text(length)
        ...     timing = timeit('hash(t)', 'from __main__ import testword as t')
        ...     print 'Length: {}, timing: {}'.format(length, timing)
        ... 
        Length: 11, timing: 0.061537027359
        Length: 20, timing: 0.0796310901642
        Length: 110, timing: 0.0631730556488
        Length: 1010, timing: 0.0606122016907
        Length: 10010, timing: 0.0613977909088
        Length: 100010, timing: 0.0607581138611
        Length: 1000010, timing: 0.0672461986542
        Length: 10000010, timing: 0.080118894577
        

        我停止生成一个 1000 万个字符的字符串,因为我懒得等待我的笔记本电脑生成一个 1 亿个字符的字符串。

        时间几乎是恒定的,因为一旦计算,值实际上会缓存在字符串对象上。

        【讨论】:

          猜你喜欢
          • 2018-04-21
          • 1970-01-01
          • 1970-01-01
          • 2018-07-20
          • 2012-04-28
          • 2022-12-28
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多