Python 键的嵌套字典或元组?

Python 键的嵌套字典或元组?,python,optimization,dictionary,Python,Optimization,Dictionary,假设有这样一个结构: {'key1' : { 'key2' : { .... { 'keyn' : 'value' } ... } } } 使用python,我试图确定两种不同方法的优缺点: {'key1' : { 'key2' : { .... { 'keyn' : 'value' } ... } } } # A. nested dictionary {('key1', 'key2', ...., 'keyn') : 'value'} # B. a dictionary with a tup

假设有这样一个结构:

{'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):

  • 内存占用
  • 插入的复杂性(考虑冲突避免算法等)
  • 查找中的复杂性

如果您需要使用
key1
keyn
的整个组合来获得
,您可以像下面我建议的那样翻转dict以获得O(nk*nv)(键数*值数)或使用上面的
元组
方法

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

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

然而,插入速度会慢一些,尽管我无法量化它的速度。您必须为每次插入至少构造一层
dict
,并测试是否存在
dict
s在以前的级别

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

tuple
tuple
方法对于插入来说既简单又快速,但可能需要更多内存,具体取决于嵌套


我知道要求之前的原始答案

为什么不呢

{'key1' : 'value', 'key2' : 'value', .... 'keyn' : 'value' } 
它只是在每个位置存储对
value
的引用,而不是
value
本身,因此内存使用量将小于嵌套的
dict
版本,并且不会比
元组
版本大很多,除非您有大量的
value
s

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

基本上,平均插入或获取一项是O(1)

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

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

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

通过这种方式,您可以添加带有just
mydict[value]的键。append(newkey)
并获得带有just
mydict[value]
的所有键,这两个键的平均值为O(1)。

无需详细说明(无论如何,它们都高度依赖于实现,并且可能会被下一个天才(genius)宣布无效,并调整字典实现):

  • 对于内存开销:每个对象都有一些开销(例如refcount和type;一个空对象是8字节,一个空元组是28字节),但哈希表需要存储哈希、键和值,并且通常使用比当前需要更多的存储桶来避免冲突。另一方面,元组不能调整大小,也不会发生冲突,即一个N元组可以简单地将N个指针分配给包含的对象并进行操作。这导致内存消耗方面的显著差异
  • 对于查找和插入复杂性(两者在这方面是相同的):无论是字符串还是元组,在CPython的dict实现中冲突都不太可能发生,并且解决得非常有效。更多的键(因为您通过将键组合到元组中来展平键空间)可能会增加冲突的可能性,更多的键也会导致更多的bucket(请注意,当前的实现试图将负载因子保持在2/3之间),这反过来会降低冲突的可能性。此外,您不需要更多的散列(嗯,对于元组散列,再进行一次函数调用和一些C级xor,但这是可以忽略的)获得一个值
你看,在性能上不应该有任何明显的差异,尽管内存有一些差异。但我认为后者不会有什么显著的差异。一个单元素的dict是140字节,一个十元素的元组也是140字节(根据Python 3.2
sys.getsizeof
),所以即使有(我的直觉说,已经不现实了)十级嵌套,您将有略大于1KB的差异-如果嵌套的dict有多个项(取决于确切的负载系数),那么差异可能会更小。对于一个拥有数百个这样的数据结构im内存的数据处理应用程序来说,这太大了,但大多数对象并没有那么频繁地创建

你应该简单地问问自己哪个模型更适合你的问题。考虑使用一组密钥,你需要一次获得一个可用值的所有键,而使用嵌套字典允许增量地到达那里。 我已经编写了一个小脚本来测试它。虽然它有一些局限性,但键是由线性分布的整数组成的(即

range(N)
),我的发现如下

通过三级嵌套,即
dict[a][b][c]
vs
dict[a,b,c]
每个子索引从0到99,我发现:

具有较大的值(
列表(x代表范围(100)
):

开放性问题
  • 为什么会这样
  • 这是否会随着不同的指数而变化,例如非连续指数
剧本 性能测试 我对嵌套字典和具有元组的字典执行了循环、检索和插入测试。它们是一级深的2000.000个值。我还对已创建元组的元组dict执行了检索和插入

这些都是结果。我认为你不能把结论和性病发展结合起来

-

-

如您所见,nesteddict通常比使用单个键的dict快。即使在没有元组创建步骤的情况下直接为keydict提供一个元组,插入仍然要慢得多。似乎额外创建内部dict的成本并不高。Def
{'value': [key1, key2, ... keyn]}
> 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))
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
>>>>>>> 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]
"