【问题标题】:How to step through a large ordered dictionary in Python 3.7?如何在 Python 3.7 中单步执行大型有序字典?
【发布时间】:2019-09-21 14:44:40
【问题描述】:

最近我一直在将一些 bash 脚本重构为 Python 3.7,作为学习练习和项目中的实际使用。最终的实现使用了一个非常大的有序字典,比如大约 2 到 3 百万个条目。以这种方式存储数据有一些显着的优势,可以降低代码复杂性和处理时间。然而,有一个任务让我难以理解:如何从已知的起点逐步浏览字典

如果我在 C 中执行此操作,我会将指针指向所需的起点并遍历指针。如果Python中有类似的操作,我不知道也找不到。我发现的所有技术似乎都将部分/全部信息复制到一个新列表中,这将非常耗时并且在我的应用程序中浪费大量内存。似乎您也无法对字典进行切片,即使它们现在默认排序。

考虑这个人为设计的拉丁字母词典示例,其键位奇怪的条目按元音和辅音分组,每个组中的条目按字母顺序排序:

dd = { #   key:  (  phonetic, letter, ascii, ebcedic, baudot, morse,  hollerith, strokes,  kind     )
    4296433290:  ( 'Alfa',     'A',    65,     193,     3,    '.-',     (12,1),     3,    'vowl'    ),
    5046716526:  ( 'Echo',     'E',    69,     197,     1,    '.',      (12,5),     4,    'vowl'    ),
    5000200584:  ( 'India',    'I',    73,     201,     6,    '..',     (12,9),     3,    'vowl'    ),
    5000971262:  ( 'Oscar',    'O',    79,     214,     24,   '---',    (11,6),     1,    'vowl'    ),
    5000921625:  ( 'Uniform',  'U',    85,     228,     7,    '..-',    (0,4),      1,    'vowl'    ),
    4297147083:  ( 'Yankee',   'Y',    89,     232,     21,   '-.--',   (0,8),      3,    'vowl'    ),
    4297256046:  ( 'Bravo',    'B',    66,     194,     25,   '-...',   (12,2),     3,    'cons'    ),
    4298140290:  ( 'Charlie',  'C',    67,     195,     14,   '-.-.',   (12,3),     1,    'cons'    ),
    5036185622:  ( 'Delta',    'D',    68,     196,     9,    '-..',    (12,4),     2,    'cons'    ),
    5036854221:  ( 'Foxtrot',  'F',    70,     198,     13,   '..-.',   (12,6),     3,    'cons'    ),
    5037458768:  ( 'Golf',     'G',    71,     199,     26,   '--.',    (12,7),     2,    'cons'    ),
    5035556903:  ( 'Hotel',    'H',    72,     200,     20,   '....',   (12,8),     3,    'cons'    ),
    5037119814:  ( 'Juliett',  'J',    74,     209,     11,   '.---',   (11,1),     2,    'cons'    ),
    5035556831:  ( 'Kilo',     'K',    75,     210,     15,   '-.-',    (11,2),     3,    'cons'    ),
    4296755665:  ( 'Lima',     'L',    76,     211,     18,   '.-..',   (11,3),     2,    'cons'    ),
    5035557110:  ( 'Mike',     'M',    77,     212,     28,   '--',     (11,4),     4,    'cons'    ),
    5037118125:  ( 'November', 'N',    78,     213,     12,   '-.',     (11,5),     3,    'cons'    ),
    5000423356:  ( 'Papa',     'P',    80,     215,     22,   '.--.',   (11,7),     2,    'cons'    ),
    5000923300:  ( 'Quebec',   'Q',    81,     216,     23,   '--.-',   (11,8),     2,    'cons'    ),
    5000969482:  ( 'Romeo',    'R',    82,     217,     10,   '.-.',    (11,9),     3,    'cons'    ),
    5035943840:  ( 'Sierra',   'S',    83,     226,     5,    '...',    (0,2),      1,    'cons'    ),
    5045251209:  ( 'Tango',    'T',    84,     227,     16,   '-',      (0,3),      2,    'cons'    ),
    5000168680:  ( 'Victor',   'V',    86,     229,     30,   '...-',   (0,5),      2,    'cons'    ),
    4296684445:  ( 'Whiskey',  'W',    87,     230,     19,   '.--',    (0,6),      4,    'cons'    ),
    5000923277:  ( 'Xray',     'X',    88,     231,     29,   '-..-',   (0,7),      2,    'cons'    ),
    4296215569:  ( 'Zulu',     'Z',    90,     233,     17,   '--..',   (0,9),      3,    'cons'    ),
}

假设我想对辅音进行一些处理。而且由于处理需要很多时间(想想几天),我想分块进行。在这种情况下,假设一次有 4 个辅音。我提前知道组开始的键,例如:

vowlbeg = 4296433290 # key of first vowel
consbeg = 4297256046 # key of first consonant

但我不知道如何利用这种预知。例如,要处理第 8 到第 11 个辅音,我能做的最好的事情是:

beg = 8 # begin processing with 8th consonant
end = 12 # end processing with 11th consonant
kind = 'cons' # desired group
i=-1
for d in dd.items():
    if d[1][-1] is not kind: continue
    i += 1
    if i < beg: continue
    if i >= end: break
    print('processing:', i, d)

这给出了所需的结果,尽管有点慢,因为我从头开始遍历整个字典,直到遇到所需的条目。

processing: 8 (5035556831, ('Kilo', 'K', 75, 210, 15, '-.-', (11, 2), 3, 'cons'))
processing: 9 (4296755665, ('Lima', 'L', 76, 211, 18, '.-..', (11, 3), 2, 'cons'))
processing: 10 (5035557110, ('Mike', 'M', 77, 212, 28, '--', (11, 4), 4, 'cons'))
processing: 11 (5037118125, ('November', 'N', 78, 213, 12, '-.', (11, 5), 3, 'cons'))

我认为我可以使用列表或字典推导更紧凑地表达这个循环,但似乎会在内存中产生巨大的重复。也许上面的方法也能做到这一点,我不是 100% 确定。

关于我的订购词典我知道的事情

  • 这些组,例如元音和辅音,确实是分组的,而不是分散的。
  • 在每个组中,条目按已知的所需顺序排序,
  • 每组的开始键

问:有没有更好的方法来做到这一点?我的备用计划是咬紧牙关,保留一组重复的元组,每组一个,以便能够对其进行切片.但这实际上会使我的记忆力翻倍,据我所知。

注意:从这个愚蠢的例子中看不出,但能够通过单个字典中的键访问条目是我的应用程序的一个巨大优势。

【问题讨论】:

  • 字典用于键查找。您正在尝试通过(部分)值进行查找。您应该实现一个索引字典,将您的值映射到查找它们所在的键序列。
  • 您是否曾经通过密钥访问数据?如果没有,请保留“有序”部分并将其列在列表中。即使是这样,您也可以将键映射到它们的索引,以便在没有重复的情况下拥有两种访问模式。
  • 我确实是通过key来访问数据,事实上,这种用法是算法的主要核心。需要按顺序浏览列表是一项次要的管理任务。

标签: python python-3.x ordereddict


【解决方案1】:

除了复制整个字典之外,还有一个更简单的方案,您只需复制另一个链接列表中的所有键。

dd_list = LinkedList("4296433290", "5046716526", "5000200584", ... "4296215569")

并且在原始字典中,在每个条目中,还保留对与该键对应的链表条目的引用:

dd = { 
    4296433290:  ( <reference to the linked-list entry of 4296433290>, 'Alfa', ...),
    5046716526:  ( <reference to the linked-list entry of 5046716526>, 'Echo', ...),
    .....
    .....
    .....
    4296215569:  ( <reference to the linked-list entry of 4296215569>, 'Zulu', ...)
}

现在如果您想在距离4297256046 5 个条目的距离处迭代 3 个条目,您只需要这样做:

entry_iterator = dd['4297256046'][0]
i = 0
while i < 5:
    # Skip 5 entries
    entry_iterator = entry_iterator.next()
    i += 1

num_iterations = 0
while num_iterations < 3:
    key = entry_iterator.value
    entry = dd[key]
    process_entry(entry)
    entry_iterator = entry_iterator.next()
    num_iterations += 1

现在我提到链表的原因是,如果您想从地图中删除任何条目,您还可以在 O(1) 时间从链表中删除相应的条目。
如果没有删除,您可以使用常规数组,并将整数数组索引保持为&lt;reference to the linked-list entry of ...&gt;

请注意,Python 默认没有任何链表数据结构。但是,您将能够在线找到大量高质量的实现。

编辑:

数组案例的示例代码:

dd_list = ["4296433290", "5046716526", "5000200584", ... "4296215569"]

dd = { 
    4296433290:  ( 0, 'Alfa', ...),
    5046716526:  ( 1, 'Echo', ...),
    .....
    .....
    .....
    4296215569:  ( 25, 'Zulu', ...)
}

entry_index = dd['4297256046'][0]
# Skip 5 entries
entry_index += 5

num_iterations = 0
while num_iterations < 3:
    key = dd_list[entry_index]
    entry = dd[key]
    process_entry(entry)
    entry_index += 1
    num_iterations += 1

【讨论】:

  • 我不确定我是否掌握了您的示例代码,但您的概念很好。我可以在单独的列表中复制键。我认为在我的应用程序中没有使用链表的理由,因为字典本质上是一个常量。
  • 该代码只是伪代码,因为 Python 中没有内置的链表。如果它是常量,那么您可以简单地使用数组而不是链表。我将编辑数组案例的代码。
  • 检查编辑。我还改进了旧代码。有一个错误。
【解决方案2】:

对于使用 Python 内置函数的简单解决方案,您可以创建一个键列表,然后从列表中的任何点开始,但会消耗一些内存来实现列表。请参阅下面的交互式会话来演示这一点。

使用这种技术在任意范围的键上进行循环应该很容易。

1> data = {id: (id, "a") for id in range(10)}

2> data
{0: (0, 'a'), 1: (1, 'a'), 2: (2, 'a'), 3: (3, 'a'), 4: (4, 'a'), 5: (5, 'a'), 6: (6, 'a'), 7: (7, 'a'), 8: (8, 'a'), 9: (9, 'a')}

3> data.keys()
dict_keys([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

4> data.keys()[5]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'dict_keys' object does not support indexing

5> keys = list(data.keys())

6> keys[5]
5

7> data[keys[5]]
(5, 'a')
  • 第 1 步:创建一些与您类似的示例数据
  • 第 2 步:展示结构
  • 第 3 步:获取结构的 dict_keys
  • 第 4 步:以 dict_keys 原生形式证明不能跳转到列表中的特定点
  • 第 5 步:将 dict_keys 实现为实际的列表结构
  • 第 6 步:演示从列表中的任意位置获取密钥
  • 第 7 步:使用任意键从 dict 中提取数据

【讨论】:

    【解决方案3】:

    根据经验,通过循环处理这样的大量数据是不可能的,因为这意味着您使用的字典大小至少是字典大小的 2 倍(根据经验,它使用的 RAM 量是字典的字节大小)。

    几个建议:

    1. 考虑将其存储到数据框中。 pandas 包被广泛采用是有原因的:它使用后端优化(如果我错了,请有人纠正我:numpy,并且通过扩展 pandas,使用一些 C 风格或实际的 C 编译)胜过任何基础 Python 可以做。对于pandasdask,这将是一项相当容易的任务,并且性能相当不错。

      # file.py
      import pandas as pd
      
      cols =  ['key', 'phonetic', 'letter', 'ascii', 'ebcedic', 'baudot', 'morse',  'hollerith', 'strokes',  'kind']
      test = pd.DataFrame(dd).transpose().reset_index()
      
      test.columns = cols
      
      def get_letters(begin, end, kind):
          return test[test['kind'] == kind].reset_index(drop=True).iloc[begin-1:end-1]
      
      output = get_letters(8,12,'cons')
      
      final = output.set_index('key').transpose().to_dict('list')
      
      # runtime >>> mean 6.82 ms, std: 93.9 us
      
    2. 如果您打算使用基本的 Python 结构,那么推导式绝对是您的最佳选择。当您尝试从另一个组 Python 对象创建一个新的“组”Python 对象(如listsdictstuples)时,理解通常比标准的“循环和附加”策略更好地扩展。 if-else 循环应该留给您实际上没有创建新的分组对象的地方。即使您在创建新的分组对象之前有一些复杂的控制流和逻辑要做,我总是选择使用理解,并且通常只是创建“帮助”函数以提高可读性。我会这样做:

      def helper(dictionary, begin, end, cons):
          filtered = {k:v for k,v in dictionary.items() if v[8] == 'cons'}
      
          return [d for n, d in enumerate(filtered.values()) if n in range(begin-1, end-1)]
      
      helper(dd,8,12,'cons')
      
      # runtime >>> mean: 1.61ms, std: 58.5 us
      

    注意:尽管运行时似乎表明基础 Python 是一种更快的机制,但我有信心说,在更大的字典上,pandas / dask 方法将优于基础代码

    【讨论】:

      【解决方案4】:

      如果您想通过 dask 尝试此操作,这里有 2 种可能的方法

      进口

      import numpy as np
      import pandas as pd
      import dask.dataframe as ddd
      from dask import delayed, compute
      from dask.diagnostics import ProgressBar
      import time
      

      定义列名列表

      h = [
          'phonetic',
          'letter',
          'ascii',
          'ebcedic',
          'baudot',
          'morse',
          'hollerith',
          'strokes',
          'kind'
          ]
      

      使用 (per this SO post) 从字典 dd 创建 Dask DataFrame

      def make_df(d):
          return pd.DataFrame.from_dict(d, orient='index')
      
      dpd = [delayed(make_df)(dd)]
      ddf = ddd.from_delayed(dpd)
      ddf.columns = h
      ddf.head()
                 phonetic letter  ascii  ebcedic  baudot morse hollerith  strokes  kind
      4296433290     Alfa      A     65      193       3    .-   (12, 1)        3  vowl
      5046716526     Echo      E     69      197       1     .   (12, 5)        4  vowl
      5000200584    India      I     73      201       6    ..   (12, 9)        3  vowl
      5000971262    Oscar      O     79      214      24   ---   (11, 6)        1  vowl
      5000921625  Uniform      U     85      228       7   ..-    (0, 4)        1  vowl
      

      获取DataFrame中的分区数

      print(ddf.npartitions)
      1
      
      • 以下 2 种 Dask 方法仅适用于 DataFrame 的一个分区

      Dask 方法 1 - 使用 .map_partitions

      • 在这里,您定义一个辅助函数来执行kind 列的切片,
      %time
      def slicer(df, kind):
          return df[df['kind']==kind]
      
      ddf2 = ddf.map_partitions(slicer, 'cons', meta=ddf.head(1))
      with ProgressBar():
          print(ddf2.reset_index().loc[slice(8-1,12-2)].compute().head())
      
      CPU times: user 3 µs, sys: 1 µs, total: 4 µs
      Wall time: 8.82 µs
      [########################################] | 100% Completed |  0.1s
               index  phonetic letter  ascii  ebcedic  baudot morse hollerith  strokes  kind
      7   5035556831      Kilo      K     75      210      15   -.-   (11, 2)        3  cons
      8   4296755665      Lima      L     76      211      18  .-..   (11, 3)        2  cons
      9   5035557110      Mike      M     77      212      28    --   (11, 4)        4  cons
      10  5037118125  November      N     78      213      12    -.   (11, 5)        3  cons
      

      Dask 方法 2 - 使用 .loc

      %time
      with ProgressBar():
          print(ddf[ddf['kind'] == 'cons'].reset_index().loc[8-1:12-2].compute().head())
      
      CPU times: user 4 µs, sys: 1 µs, total: 5 µs
      Wall time: 9.06 µs
      [########################################] | 100% Completed |  0.1s
               index  phonetic letter  ascii  ebcedic  baudot morse hollerith  strokes  kind
      7   5035556831      Kilo      K     75      210      15   -.-   (11, 2)        3  cons
      8   4296755665      Lima      L     76      211      18  .-..   (11, 3)        2  cons
      9   5035557110      Mike      M     77      212      28    --   (11, 4)        4  cons
      10  5037118125  November      N     78      213      12    -.   (11, 5)        3  cons
      

      熊猫

      %time
      df = pd.DataFrame.from_dict(dd, orient='index', columns=h)
      print(df[df['kind']=='cons'].reset_index().loc[slice(8-1,12-2)].head())
      
      CPU times: user 3 µs, sys: 1 µs, total: 4 µs
      Wall time: 8.82 µs
               index  phonetic letter  ascii  ebcedic  baudot morse hollerith  strokes  kind
      7   5035556831      Kilo      K     75      210      15   -.-   (11, 2)        3  cons
      8   4296755665      Lima      L     76      211      18  .-..   (11, 3)        2  cons
      9   5035557110      Mike      M     77      212      28    --   (11, 4)        4  cons
      10  5037118125  November      N     78      213      12    -.   (11, 5)        3  cons
      

      编辑

      当我从他的answer 运行@zero 的方法时,我得到了

      %time
      print(helper(dd,8,12,'cons'))
      Wall time: 8.82 µs
      

      【讨论】:

        猜你喜欢
        • 2019-07-17
        • 1970-01-01
        • 2013-03-13
        • 2018-11-02
        • 1970-01-01
        • 2014-11-10
        • 1970-01-01
        • 2017-09-08
        • 1970-01-01
        相关资源
        最近更新 更多