密钥的嵌套字典或元组?

时间:2011-08-22 12:56:17

标签: python optimization dictionary

假设有这样的结构:

{'key1' : { 'key2' : { .... { 'keyn' : 'value' } ... } } }

使用python,我试图确定两种不同方法的优点/缺点:

{'key1' : { 'key2' : { .... { 'keyn' : 'value' } ... } } } # A. nested dictionary
{('key1', 'key2', ...., 'keyn') : 'value'} # B. a dictionary with a tuple used like key

然后我有兴趣知道,最好的(A或B)是什么:

  • 记忆占用
  • 插入的复杂性(考虑避免碰撞的算法......等)
  • 查找中的复杂性

4 个答案:

答案 0 :(得分:10)

不进入细节(无论如何都是高度依赖于实现的,并且可能会被下一个天才无效并调整字典实现):

  • 对于内存开销:每个对象都有一些开销(例如引用计数和类型;空对象是8个字节,空元组是28个字节),但是哈希表需要存储哈希,键和值,并且通常使用多个桶目前需要避免碰撞。另一方面,元组不能调整大小并且不会发生冲突,即N元组可以简单地将N个指针分配给包含的对象并完成。这导致内存消耗明显不同。
  • 对于查找和插入复杂性(两者在这方面是相同的):无论是字符串还是元组,在CPython的dict实现中碰撞都不太可能,并且非常有效地解决。更多的键(因为你通过组合元组中的键来平整键空间)似乎增加了碰撞的可能性,更多的键也导致更多的桶(当前实现的AFAIK试图将负载因子保持在2/3之间),反过来使碰撞不太可能发生。此外,你不需要更多的散列(好吧,还有一个函数调用和一些C级xor-for for tuple hash,但这是可以接受的)来得到一个值。

你知道,虽然存在一些记忆差异,但性能上应该没有明显的差异。我想,后者不会引人注目。单元素dict是1​​40字节,十元素元组也是140字节(根据Python 3.2 sys.getsizeof)。因此即使使用(已经不切实际的,我的直觉)十级嵌套,你的差异也会略微超过一KB - 如果嵌套的dicts有多个项目(取决于确切的加载因子),可能会更少。对于拥有数百个这样的数据结构内存的数据运算应用程序来说,这太过分了,但大多数对象都不是经常创建的。

您应该问自己哪种型号更适合您的问题。考虑到第二种方式要求您一次获得值的所有键,而第二种方法允许逐步获得值。

答案 1 :(得分:2)

如果你需要使用key1keyn的整个组合来获取value,你可以按照我在下面建议的O(nk * nv)翻转dict(数量为密钥*值的数量)或使用上面的tuple方法。

假设您需要在插入时构建tuple,并在需要获取值时再次构建{两者将是O(nk),其中nk是键的数量。

嵌套的dict版本可能会更加节省空间,如果你的嵌套相当深(有很多值共享一个部分有序的键列表),获取一个值仍然是O(nk) ,但可能比元组版本慢。

然而,插入速度会慢一些,但我无法量化它的速度。您必须为每个插入构建至少一层dict,并测试是否存在 dict以前的水平。

递归defaultdictmany recipes,可以简化从编码角度的插入,但实际上并不会加快速度。

tuple方法更简单,插入更快,但可能会占用更多内存,具体取决于您的嵌套。


在我了解要求之前的原始答案

为什么不

{'key1' : 'value', 'key2' : 'value', .... 'keyn' : 'value' } 

它只是在每个位置存储value的引用,而不是value本身,所以内存使用量 less 比嵌套的dict版本,并且没有比tuple版本大得多,除非你有非常多的value

有关Python标准类型操作的时间复杂性,请参阅the Python wiki

基本上,平均插入或获得一个项目是O(1)。

获取值的所有键平均为O(n):

[key for key in mydict if mydict[key] == value]

但是,如果通常的操作是添加密钥或搜索所有密钥,则会翻转dict。你想要:

{'value': [key1, key2, ... keyn]}

通过这种方式,您只需mydict[value].append(newkey)即可添加密钥,只需mydict[value]即可获得所有密钥,两者的平均值均为O(1)。

答案 2 :(得分:0)

内存消耗测试

我写了一个小脚本来测试它。但是它有一些限制,键是由线性分布的整数(即range(N))组成的,我的发现如下。

使用3级嵌套,即dict[a][b][c] vs dict[a,b,c],其中每个子索引从0到99,我发现以下内容:

使用较大的值(list(x for x in range(100))):

> memory.py nested 
Memory usage: 1049.0 MB
> memory.py flat  
Memory usage: 1149.7 MB

并且值较小([]):

> memory.py nested
Memory usage: 134.1 MB
> memory.py flat
Memory usage: 234.8 MB

打开问题

  • 为什么会这样?
  • 这会改变不同的指数,例如非连续的?

脚本

#!/usr/bin/env python3
import resource
import random
import itertools
import sys
import copy
from os.path import basename
from collections import defaultdict

# constants
index_levels = [100, 100, 100]
value_size   = 100 # try values like 0

def memory_usage():
    return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss

_object_mold = list(x for x in range(value_size)) # performance hack
def create_object():
    return copy.copy(_object_mold)

# automatically create nested dict
# http://code.activestate.com/recipes/578057-recursive-defaultdict/
f = lambda: defaultdict(f)
my_dict = defaultdict(f)

# options & usage
try:
    dict_mode = sys.argv[1]
    if dict_mode not in ['flat', 'nested']: # ugly hack
        raise Error()
except:
    print("Usage: {} [nested | flat]".format(basename(sys.argv[0])))
    exit()

index_generator = [range(level) for level in index_levels]

if dict_mode == "flat":
    for index in itertools.product(*index_generator):
        my_dict[index] = create_object()
elif dict_mode == "nested":
    for index in itertools.product(*index_generator):
        sub_dict = my_dict
        for sub_index in index[:-1]:          # iterate into lowest dict
            sub_dict = sub_dict[sub_index]
        sub_dict[index[-1]] = create_object() # finally assign value

print("Memory usage: {:.1f} MB".format(memory_usage() / 1024**2))

答案 3 :(得分:0)

性能测试

我对嵌套字典和带元组的字典进行了循环,检索和插入的测试。它们是一层深的2000.000值。我还使用已创建的元组对元组dict进行了检索和插入。

这些是结果。我认为您不能真正将结论绑定到标准开发人员。

-

keydictinsertion: Mean +- std dev: 615 ms +- 42 ms  
keydictretrieval: Mean +- std dev: 475 ms +- 77 ms  
keydictlooping: Mean +- std dev: 76.2 ms +- 7.4 ms  

nesteddictinsertion: Mean +- std dev: 200 ms +- 7 ms  
nesteddictretrieval: Mean +- std dev: 256 ms +- 32 ms  
nesteddictlooping: Mean +- std dev: 88.5 ms +- 14.4 ms  

Test were the tuple was already created for the keydict  
keydictinsertionprepared: Mean +- std dev: 432 ms +- 26 ms  
keydictretrievalprepared: Mean +- std dev: 267 ms +- 14 ms

-

如您所见,nesteddict通常比使用单个键的dict更快。即使在不使用元组创建步骤的情况下直接给keydict一个元组,插入仍然要慢得多。似乎额外创建一个内部dict不需要花费太多。 Defaultdict可能有一个快速的实现。实际上,不必创建元组时,检索实际上几乎相等,与循环相同。

使用perf从命令行进行测试。脚本如下。

>>>>>>> nesteddictinsertion
python -m perf timeit -v -s "
from collections import defaultdict
" " 
d = defaultdict(dict)
for i in range(2000):
    for j in range(1000):
        d[i][j] = 1
"
>>>>>>> nesteddictlooping
python -m perf timeit -v -s "
from collections import defaultdict
d = defaultdict(dict)
for i in range(2000):
    for j in range(1000):
        d[i][j] = 1
" "
for i, inner_dict in d.items():
    for j, val in inner_dict.items():
        i
        j
        val
"
>>>>>>> nesteddictretrieval
python -m perf timeit -v -s "
from collections import defaultdict
d = defaultdict(dict)
for i in range(2000):
    for j in range(1000):
        d[i][j] = 1
" "
for i in range(2000):
    for j in range(1000):
        d[i][j]
"
>>>>>>> keydictinsertion
python -m perf timeit -v -s "
from collections import defaultdict
" " 
d = {}
for i in range(2000):
    for j in range(1000):
        d[i, j] = 1
"
>>>>>>> keydictinsertionprepared
python -m perf timeit -v -s "
from collections import defaultdict
keys = [(i, j) for i in range(2000) for j in range(1000)]
" " 
d = {}
for key in keys:
    d[key] = 1
"
>>>>>>> keydictlooping
python -m perf timeit -v -s "
from collections import defaultdict
d = {}
for i in range(2000):
    for j in range(1000):
        d[i, j] = 1
" "
for key, val in d.items():
    key
    val
"
>>>>>>> keydictretrieval
python -m perf timeit -v -s "
from collections import defaultdict
d = {}
for i in range(2000):
    for j in range(1000):
        d[i, j] = 1
" "
for i in range(2000):
    for j in range(1000):
        d[i, j]
"
>>>>>>> keydictretrievalprepared
python -m perf timeit -v -s "
from collections import defaultdict
d = {}
keys = [(i, j) for i in range(2000) for j in range(1000)]
for key in keys:
    d[key] = 1
" "
for key in keys:
    d[key]
"