【问题标题】:How to extact elements from dictionary of lists efficiently?如何有效地从列表字典中提取元素?
【发布时间】:2021-09-23 10:21:10
【问题描述】:

这是我最初的字典:

dic = {'key1': [2,3],
       'key2': [5,1],
       'key3': [6,8]}

注意:出于示例的目的,我使用简单的数字 2,3 等(我这边的 dfs 列表)。

对于每个键,我想提取第一个元素并得到以下结果:

dic2 = {'key1': 2,
        'key2': 5,
        'key3': 6}

是否可以在不使用缓慢的 for 循环的情况下做到这一点? 字典很大......

非常感谢您的帮助。

【问题讨论】:

  • 为什么你认为 for 循环很慢?除了以某种方式遍历字典之外,您还有什么other替代方法??!
  • "字典挺大"有多大?
  • 如果您希望在此转换后只访问字典的几个键,您可以编写一个仅返回第一个元素的函数并使用它而不是调用dic2.get(...)
  • 或者,实际上是一个包装器
  • @plonfat 太小了。循环超过 12k 个键并使用循环手动创建副本只需不到一毫秒。

标签: python dictionary


【解决方案1】:

如果您希望在此转换后只访问字典的几个键,您可以编写一个包装器;类似:

class ViewFirst:
  def __init__(self, original):
    self.original = original
  def __getitem__(self, key):
    return self.original[key][0]

另一个选择是基于defaultdict;这将使您可以执行诸如将新值分配到字典(新的或现有的键)中,同时仍从原始字典中检索其他值的操作:

class DictFromFirsts(collections.defaultdict):
  def __init__(self, original):
    self.original = original
  def __missing__(self, key):
    return self.original[key][0]

编辑:根据 cmets 中的讨论,这是针对特定场景的专用方法。对于通用用途,更喜欢其他答案中的一种方法,例如U12-Forward's dict comprehension{k: v[0] for k, v in dic.items()};它清晰明了,这通常是更重要的问题。

【讨论】:

  • 我很难弄清楚如何让您的答案发挥作用。
  • 它特别适用于您希望仅访问几个键的用例,这些键是从其他地方获得的;你可以去vf = ViewFirst(dic); print(vf['key1'])
  • 我为什么要使用它而不是 dic['key1'][0]
  • 在某些情况下,这类包装器非常有用;在这个问题中,没有上下文,它只是回答“如何在没有循环的情况下做到这一点”的一种方式。请注意,如果您没有遍历整个内容,那么您可能只是根据来自其他地方的密钥的短列表挑选出一些项目。即使原始字典本身是某种包装器(可能用于键值存储或其他磁盘或网络访问,可能比内存大,甚至完全无限(动态生成)),这个答案也会起作用
  • 是的,here's 是我昨天刚刚编写的包装器/代理/适配器的示例,提供了 m*n 矩阵的视图,因为搜索功能仅适用于一维数据,并且因为它只会访问 log(mn) 元素(因此构建所有 mn 元素的列表成本太高)。还有@MarcoD.G.
【解决方案2】:

字典理解是这里的方法:

{k: v[0] for k, v in dic.items()}

或者使用operator.itemgetter:

>>> from operator import itemgetter
>>> dict(zip(dic, map(itemgetter(0), dic.values())))
{'key1': 2, 'key2': 5, 'key3': 6}
>>> 

【讨论】:

  • 注意,这仍然是使用for循环
  • @juanpa.arrivillaga 编辑map
  • 是的,它基本上仍然使用 for 循环,就在解释器级别之下。请注意,这将比常规循环快得多。
  • 感谢您的回答,所以我猜 operator.itemgetter 将是这里最快的选择?
  • @plonfat 它们都将花费相似的时间,类似于等效的 for 循环。列表推导和地图不是为了性能
【解决方案3】:

让我们看看 for 循环到底有多“慢”。我的解决方案:

dic2 = {}
for k, (dic2[k], _) in dic.items():
    pass

用您的玩具词典进行基准测试:

    600 ns      602 ns      603 ns  U12_Forward_1
   1019 ns     1025 ns     1027 ns  U12_Forward_2
   1347 ns     1350 ns     1355 ns  user1740577
    441 ns      442 ns      443 ns  dont_talk_just_code

按照您的评论,使用包含 12k 个项目的“大”字典进行基准测试:

1412624 ns  1414927 ns  1418089 ns  U12_Forward_1
1687464 ns  1690134 ns  1696759 ns  U12_Forward_2
1961205 ns  1986729 ns  2005884 ns  user1740577
1248901 ns  1260306 ns  1261295 ns  dont_talk_just_code

以上是在on tio.run完成的,我使用它是因为它的高速和速度稳定性。遗憾的是,它没有提供 Marco 的回答所需的 cytoolz,所以我不能包括在内。然后@user1740577 指责我撒谎,因为我没有包含它,所以这里是 replit.com 的结果,我可以(请注意,如果你在那里运行它并且没有付费帐户,你的时间会慢一些) :

    475 ns      476 ns      484 ns  U12_Forward_1
    804 ns      805 ns      807 ns  U12_Forward_2
   1075 ns     1079 ns     1082 ns  user1740577
    442 ns      444 ns      448 ns  Marco_DG
    360 ns      360 ns      360 ns  dont_talk_just_code

1060461 ns  1061449 ns  1071588 ns  U12_Forward_1
1294079 ns  1330157 ns  1706065 ns  U12_Forward_2
1593082 ns  1594114 ns  1596703 ns  user1740577
1268663 ns  1274264 ns  1286715 ns  Marco_DG
 964445 ns   965971 ns   966333 ns  dont_talk_just_code

完整的基准代码(也有at replit:

from timeit import repeat
from functools import partial
from operator import itemgetter
from cytoolz import valmap , first

def U12_Forward_1(dic):
    return {k: v[0] for k, v in dic.items()}

def U12_Forward_2(dic):
    return dict(zip(dic, map(itemgetter(0), dic.values())))

def user1740577(dic):
    return dict(zip(dic.keys(),list(list(zip(*dic.values()))[0])))

def Marco_DG(dic):
    return valmap(first, dic)

def dont_talk_just_code(dic):
    dic2 = {}
    for k, (dic2[k], _) in dic.items():
        pass
    return dic2

funcs = U12_Forward_1, U12_Forward_2, user1740577, Marco_DG, dont_talk_just_code

def bench(dic, number):
    expect = funcs[0](dic)
    for func in funcs:
        result = func(dic)
        print(result == expect, func.__name__)
    print()

    for _ in range(3):
        for func in funcs:
            ts = sorted(repeat(partial(func, dic), number=number))[:3]
            print(*('%7d ns ' % (t / number * 1e9) for t in ts), func.__name__)
        print()

bench({'key1': [2,3], 'key2': [5,1], 'key3': [6,8]}, 100000)
bench({f'key{i}': [i,42] for i in range(12000)}, 30)

【讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • 这个循环是如何工作的?为什么dic2[k] 在该循环语句中将第一个元素分配给该键?因为我们通常不得不说dic2[k] = foo
  • @KarlWilhelm That's just how assignments work 在 Python 中。它是 for 循环的目标列表中的一个目标,所以它被分配了。从 Python 的角度来看,这并不奇怪,真的。从某种意义上说,这很不寻常,因为大多数人选择了一个像你的 foo 这样的变量的绕道。
【解决方案4】:

就我个人而言,我会选择一个在后台使用 Cython 的库:cytoolz

pip3 install cytoolz
from cytoolz import valmap , first

dic = {'key1': [2,3],
       'key2': [5,1],
       'key3': [6,8]}


dic2 = valmap(first, dic)

哪个是最好的解决方案?

我将使用我的函数基准测试扩展@don'ttalkjustcode 测试,我仍在尝试找出如何通过@Jiří Baum 测试它。

一般情况(n 个元素数组):@U12-正向解 n° 1

使用@don'ttalkjustcode 通用代码for k, (dic2[k], *_)

三元素字典:

    457 ns      457 ns      467 ns  U12_Forward_1
    775 ns      775 ns      776 ns  U12_Forward_2
   1021 ns     1021 ns     1036 ns  user1740577
    430 ns      430 ns      432 ns  Marco_DG
    679 ns      679 ns      683 ns  dont_talk_just_code

12k 元素字典:

 992967 ns   997872 ns   998554 ns  U12_Forward_1
1251728 ns  1254163 ns  1254897 ns  U12_Forward_2
1434998 ns  1436245 ns  1440789 ns  user1740577
1219357 ns  1219453 ns  1225301 ns  Marco_DG
2208451 ns  2213086 ns  2214531 ns  dont_talk_just_code

特殊情况(2 个元素数组):@don'ttalkjustcode 解决方案

三元素字典:

    422 ns      422 ns      422 ns  marco_dg
    462 ns      462 ns      462 ns  U12_Forward_1
    765 ns      766 ns      769 ns  U12_Forward_2
   1076 ns     1081 ns     1088 ns  user1740577
    341 ns      341 ns      341 ns  dont_talk_just_code

12k 元素字典:

1206537 ns  1208705 ns  1211105 ns  marco_dg
1009374 ns  1011324 ns  1011989 ns  U12_Forward_1
1232356 ns  1232728 ns  1251990 ns  U12_Forward_2
1380953 ns  1382381 ns  1390140 ns  user1740577
 848863 ns   850010 ns   850450 ns  dont_talk_just_code

【讨论】:

    【解决方案5】:

    你可以试试这个:

    dict(zip(dic.keys(),list(list(zip(*dic.values()))[0])))
    

    输出:

    {'key1': 2, 'key2': 5, 'key3': 6}
    

    【讨论】:

    • 好主意!这个解决方案会比上面提出的更快吗?
    猜你喜欢
    • 1970-01-01
    • 2020-05-24
    • 2021-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-18
    • 2021-05-10
    相关资源
    最近更新 更多