Fastest way to generate a dict from list where key == value
我有一个清单,说:
1 2
| NUM = 100
my_list = list(range(NUM)) |
我想生成一个dict,其中键等于值,如:
1
| my_dict = {item: item for item in my_list} |
要么:
1
| my_dict = dict(zip(my_list, my_list)) |
我已经运行了一些微基准测试,看起来他们有相似的速度,但我希望第二个更快,因为循环应该在C中发生。
例如,以下构造:
1
| my_dict = {key: SOMETHING for key in keys} |
转化为更快:
1
| my_dict = dict.fromkeys(k, SOMETHING) |
所以,我的问题是:{x: x for x in my_list}是否有类似的这种结构?
编辑
我检查了dir(dict),似乎没有任何方向(我希望它被称为像dict.fromitems())。
编辑2
像dict.fromitems()这样的方法比这个特定的用例有更广泛的应用,因为:
1
| dict.fromitems(keys, values) |
原则上可以替代两者:
1
| {k, v for k, v in zip(keys, values)} |
和:
-
不,那里没有。
-
我真的不明白为什么有人会首先需要这样的东西......你能给我们一个用例吗?也许我们可以找到一种更好的方式来做你想做的事情
-
dict(zip版本进行了两次函数调用(尽管它们是C调用,因此比Python调用更快)。但zip必须构建一堆元组,虽然这是一个廉价的操作,但dict comp避免了这种情况。
-
时间问题是代码的瓶颈吗?
-
还有,弗雷德说。您打算在任何阶段更改值吗?如果没有,为什么不使用一套呢?
-
@Fred这用于建模多对一关系,我希望不同的项目映射到相同的值,其中一个关系是项目本身。最后,我想使用类似my_dict['a']和my_dict['alice']的内容来获取相同的信息。
-
@ PM2Ring我曾想过类似速度的东西。我不知道如何使用set()建模多对一关系。
-
为了建立"多对一"关系,我使用图表(参见networkx)。
-
@IMCoins感谢你的建议,但这将是一个带炮弹的苍蝇......
-
既然我知道你的用例,那么key == value的东西是有意义的,而set就不会有任何用处。 :)
-
@ PM2Ring实际上,dict.fromitems()的快速C实现将取代{k: v for k, v in zip(keys, values)}或dict(zip(keys, values))构造,这比需要k == v更通用和有用,我认为无论如何都可能有更快的构造。
-
dict.fromitems()不存在,因为dict()本身已经执行了该功能。 dict(zip(my_list, my_list))已经将项目传递给构造函数,为什么有一个单独的dict.fromitems()执行相同的操作?如果您打算让该类方法有不同的输入,请不要将其命名为dict.fromitems()。
-
密钥和值相同的用例并不常见,因此我怀疑是否会支持比{k: k for k in iterable}更快的路径。这是迭代和字典构建,在这里花费时间,而不是为循环执行字节码,这就是zip()不快的原因。 dict.fromkeys(mylist)与dict(zip(mylist, mylist))和{k: k for k in mylist}处于同一速度*的范围内。
不,没有更快的方法可供字典使用。
这是因为性能成本全部来自迭代器处理每个项目,计算其哈希值并将密钥插入字典数据哈希表结构(包括动态增长这些结构)。相比之下,执行字典理解字节码实际上是微不足道的。
dict(zip(it, it)),{k: k for k in it}和dict.fromkeys(it)的速度都接近:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| >>> from timeit import Timer
>>> tests = {
... 'dictcomp': '{k: k for k in it}',
... 'dictzip': 'dict(zip(it, it))',
... 'fromkeys': 'dict.fromkeys(it)',
... }
>>> timings = {n: [] for n in tests}
>>> for magnitude in range(2, 8):
... it = range(10 ** magnitude)
... for name, test in tests.items():
... peritemtimes = []
... for repetition in range(3):
... count, total = Timer(test, 'from __main__ import it').autorange()
... peritemtimes.append(total / count / (10 ** magnitude))
... timings[name].append(min(peritemtimes)) # best of 3
...
>>> for name, times in timings.items():
... print(f'{name:>8}', *(f'{t * 10 ** 9:5.1f} ns' for t in times), sep=' | ')
...
dictcomp | 46.5 ns | 47.5 ns | 50.0 ns | 79.0 ns | 101.1 ns | 111.7 ns
dictzip | 49.3 ns | 56.3 ns | 71.6 ns | 109.7 ns | 132.9 ns | 145.8 ns
fromkeys | 33.9 ns | 37.2 ns | 37.4 ns | 62.7 ns | 87.6 ns | 95.7 ns |
这是每种技术的每件物品成本表,从100到1000万件。随着增加哈希表结构的额外成本累积,时间会上升。
当然,dict.fromkeys()可以更快地处理项目,但它不比其他进程快一个数量级。它的(小)速度优势不是来自能在这里迭代C;差别在于完全不必每次迭代都更新值指针;所有键都指向单值引用。
zip()比较慢,因为它构建了额外的对象(为每个键值对创建一个2项元组不是一个免费的操作),并且它增加了进程中涉及的迭代器数量,你来自单个迭代器对于字典理解和dict.fromkeys(),到3个迭代器(dict()迭代委托,通过zip(),到键和值的两个独立的迭代器)。
向dict类添加单独的方法以在C中处理它是没有意义的,因为
反正不是一个常见的用例(创建一个带键和值相等的映射不是常见的需求)
无论如何,C语言的速度都不会比字典理解速度快得多。
-
感谢您的见解。但是,我相信从两个迭代中创建dict的构造可能是一个足够大的用例来证明在C中实现的dict.fromitems(keys, values)方法。这将明显快于理解或使用zip() ,类似于list(items)比[i for i in items]更快。我希望在这个特定情况下可能有一些技巧。
-
@ norok2:list()可以更快,因为构建列表对象操作简单,甚至可以在items的大小已知时预先调整新列表对象的大小。列表组合大多慢,因为它必须进行动态调整大小,因为你不能再添加大小提示了项目。字典不能预先调整大小,因此dict.fromitems(keys, values)不会更快。
-
@ norok2:即使速度有了很小的改进(zip()必须为每对创建一个元组,所以你可以用专用的dict.fromitems()方法来避免它),你必须说服Python核心devs值得添加这种方法的额外费用。成本是:未来的维护,文档更新,使用混淆(其中dict(zip(...))已经提供相同的功能)。我不认为他们会咬人,因为没有真正的好处,没有足够的用例需要这种优化水平。
使用这里的答案结果,我们创建了一个子类defaultdict的新类,并覆盖其缺少的属性以允许将键传递给default_factory:
1 2 3 4 5 6 7 8
| from collections import defaultdict
class keydefaultdict(defaultdict):
def __missing__(self, key):
if self.default_factory is None:
raise KeyError(key)
else:
ret = self[key] = self.default_factory(key)
return ret |
现在,您可以通过执行以下操作来创建您要查找的字典类型:
1
| my_dict = keydefaultdict(lambda x: x) |
然后,只要您需要为不映射到自己的键进行映射,您只需更新这些值。
计时。
子类defaultdict:
1 2 3
| %%timeit
my_dict = keydefaultdict(lambda x: x)
for num in some_numbers: my_dict[num] == num |
结果:
1
| 4.46 s ± 71.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) |
Dict理解
1 2 3
| %%timeit
my_dict = {x: x for x in some_numbers}
for num in some_numbers: my_dict[num] == num |
结果:
1
| 1.19 s ± 20.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) |
当您需要访问大约17%的原始值时,这两者变得可比较。如果您需要更少,那就更好了:
仅访问原始值的一部分
子类defaultdict:
1 2 3 4
| %%timeit
frac = 0.17
my_dict = keydefaultdict(lambda x: x)
for num in some_numbers[:int(len(some_numbers)*frac)]: my_dict[num] == num |
结果:
1
| 770 ms ± 4.69 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) |
Dict理解
1 2 3 4
| %%timeit
frac = 0.175
my_dict = {x: x for x in some_numbers}
for num in some_numbers[:int(len(some_numbers)*frac)]: my_dict[num] == num |
结果:
1
| 781 ms ± 4.03 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) |
-
哇,这挖到了内部!当你说subclass the default dict答案中my_dict的类型是什么时?这是一个普通的词典吗?你可以timeit并根据提供的示例给出基准吗?你能添加如何将其传递给列表吗?
-
@devssh my_dict将是以下所有的实例:dict,defaultdict和keydefaultdict。
-
您能否更具体地说明如何在给定的list中使用它?还有,可以请你制作一些时间吗?
-
即使是你引用的帖子也说这种方法并不干净,这种子类化方法从2010年开始就很老了,我准备好了基准测试代码,如何将列表传递给这个keydefaultdict?有人在下面的其中一篇SO帖子中将代码计时,他们说它并不比清洁代码更快。
-
@devssh您没有将列表传递给它。当你执行my_dict[k]时,如果k不是现有密钥,则调用工厂函数从密钥创建一个值(lambda x:x返回密钥本身),使用新的键值对更新dict ,并返回值。
-
但你从未在代码中的任何地方调用my_list。那么如何将my_list的类型强制转换为my_dict。这不是解决方案的唯一部分吗?你打算以后做for x in my_list: my_dict[x]吗?在声明这种类型后我不知道我做了什么,所以我无法进行基准测试。
-
@ norok2我已经更新了一些时间。节省来自于初始化字典,其速度提高了6个数量级。初始化后您将使用字典的方式最终将决定性能。
-
@devssh有人说它不干净。这是一个透视问题。另外,你如何在python中继承?据我所知,在过去的8年里,情况并没有改变。
-
@devssh你熟悉defaultdict吗?它以同样的方式工作。唯一的区别是,它不是仅具有"固定"默认值,而是具有取决于键的值(特别是它是键)。现在分析它取决于您认为它将如何使用。恕我直言,初始化应该还包括实际创建所有键值对,所以我将for x in my_list: my_dict[x]放在初始化步骤中以对其进行分析。此处显示的分析是否提供有用信息取决于您计划如何使用它。
-
事实证明,这种方法很快,但节省的费用很少。这看起来有点矫枉过正,而且仅限于这个小小的利基市场。很高兴知道它在性能方面具有可比性,之前对此一无所知。
-
@ PMende感谢您的建议。在这个想法很有意思的前提下,这不会限制字典中的keys,对于那些不存在的东西,它将表现为身份。这与dict与key == value的行为完全不同。当然,可以扩展该方法以使其表现得像普通的dict。我不确定这会更快。
-
@ norok2我不确定你的意思是"a dict和key == value"。我的建议是构建一个dict,其键值等于键,就像你在原始帖子中请求一样。
-
这肯定不会更快,因为您为每个值添加了一个函数调用,而不是让值可以预先获得。 Python函数调用在这里相对昂贵。
-
@MartijnPieters好吧,尽管你的断言,翻译不同意你的看法。我要去翻译。
-
@ PMende:你的'添加'键值对的循环需要921ms。如何通过词典理解来创建它比620ms更快?
-
请注意,971毫秒是多次运行的平均时间,第一次运行的速度会慢一些。计时器完成的额外运行将不再触发__missing__方法,因此速度更快。
-
@MartijnPieters你是否计划在没有访问价值观的情况下使用你的dict?可能不是。我的方法会更快。
-
@ PMende:不,因为971ms是7次运行的平均值,第一次运行是在进行实际的密钥实现。这是第一次运行必须超过620ms的dict理解。你不能非常有效地计时,因为你需要为每个测试重置你的dict子类。
-
@ PMende:你需要创建一个序列:testdicts = iter([keydefaultdict(lambda x: x) for _ in range(1000)]),然后是时间%timeit d = next(testdicts); for key in some_numbers: d[key] = key。希望1000为IPython提供足够的空keydefaultdict()实例来运行它的测试。或者使用timeit模块运行所有测试,以便您可以精确控制重复次数。
-
无论如何,您可以尝试使用随机整数来测试__missing__方法,并将该数字乘以len(some_numbers)作为实现许多数字的成本的良好近似值。这比dict理解做同样的工作要慢,因为dict comp没有执行if测试,没有帧调用栈来处理,没有lambda来调用。
-
@MartijnPieters是的,你说得对,我没有正确地解决完整字典的创建问题。我编辑了我的答案来表明这一点。但是,性能再次取决于用例。我已经证明,如果你不需要访问所有密钥,我的方法会更快(显然)。
-
你甚至不应该使用lambda。只需子类dict并让其__missing__将值设置为键。