Как пройти большой упорядоченный словарь в Python 3.7? - PullRequest
4 голосов
/ 03 мая 2019

В последнее время я реорганизовал некоторые сценарии 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%.

Что я знаю о моем заказном словаре

  • группы, например, гласные и согласные, действительно сгруппированы и не разбросаны.
  • внутри каждой группы, записи сортируются в известном желаемом порядке,
  • начальный ключ каждой группы

В: Есть ли лучший способ сделать это? Мой план резервного копирования - просто укусить пулю и сохранить дубликат набора кортежей, по одному на группу, чтобы иметь возможность разрезать его. Но это, по сути, удвоит мою память, насколько я понимаю.

Примечание: из этого глупого примера это не очевидно, но возможность доступа к записям по ключам в одном словаре - ОГРОМНОЕ преимущество в моем приложении.

Ответы [ 4 ]

3 голосов
/ 03 мая 2019

Вместо того, чтобы делать копию всего словаря, существует более простая схема, в которой вам просто нужно сделать копию всех ключей в другом связанном списке .

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', ...)
}

Теперь, если вы хотите повторить 3 записина расстоянии 5 записей от 4297256046 вам просто нужно сделать:

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 голосов
/ 03 мая 2019

Для простого решения, использующего встроенные модули 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, используя произвольный ключ
1 голос
/ 03 мая 2019

из опыта, работа с большим объемом данных, таких как этот цикл, невозможна, потому что это означает, что вы используете как минимум в 2 раза больше словаря (по опыту он использует в два раза больше оперативной памятиразмер байта словаря).

Несколько предложений:

  1. Посмотрите, как сохранить это в кадре данных.Есть причина, по которой пакет pandas получил широкое распространение: он использует бэкэнд-оптимизации (кто-то поправит меня, если я ошибаюсь: numpy, и с помощью расширений pandas использует некоторые C-style или фактические C-компиляции), который превосходит все базовые возможности Pythonделать.Это было бы довольно простой задачей с pandas или dask и работало бы достаточно хорошо.

    # 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 (например, lists, dicts или tuples) из другого группового объекта Python, понимания часто масштабируются намного лучше, чем стандартная тактика «цикл и добавление».Циклы 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 превзойдет базовый код

0 голосов
/ 03 мая 2019

Если вы хотите попробовать это с 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'
    ]

Создать Dask DataFrame из словаря dd, используя (за этот пост SO )

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

EDIT

Когда я запускаю подход @ ноля из его ответа , я получаю

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