如何在 Python 3.7 中遍历大型有序字典?

How to step through a large ordered dictionary in Python 3.7?

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

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

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

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% 确定。

关于我订购的字典我所知道的事情

问:有更好的方法吗?我的备份计划是硬着头皮保留一组重复的元组,每组一个,以便能够切片它。但据我所知,这基本上会使我的记忆力加倍。

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

与其复制整个字典,还有一个更简单的方案,您只需要复制另一个链表中的所有键即可.

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) 时间内从链表中删除相应的条目.
如果没有删除,您可以只使用常规数组,并将整数数组索引保留为 <reference to the linked-list entry of ...>.

注意 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

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

一些建议:

  1. 考虑将其存储到数据框中。 pandas 包被广泛采用是有原因的:它使用后端优化(如果我错了,有人纠正我:numpy,并且通过扩展 pandas,使用一些 C 风格或实际的 C 编译)这胜过 base 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 对象创建新的 "group" Python 对象(如 listsdictstuples)时,理解通常比标准 "loop and append" 策略更好。 if-else 循环应该留给您实际上没有创建新分组对象的地方。即使在创建新的分组对象之前你有一些复杂的控制流和逻辑要做,我总是选择使用理解,并且通常只创建 "helper" 函数以提高可读性。我会这样做:

    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
    

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

对于使用 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 步:使用任意键从字典中提取数据

如果你想用 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'
    ]

使用(根据

从字典 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

Pandas

%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

编辑

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

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