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

Учитывая, что у меня есть два списка, например:

l1 = ['a', 'c', 'b', 'e', 'f', 'd']
l2 = [
    'x','q','we','da','po',
    'a', 'el1', 'el2', 'el3', 'el4',
    'b', 'some_other_el_1', 'some_other_el_2',
    'c', 'another_element_1', 'another_element_2',
    'd', '', '', 'another_element_3', 'd4'
]

и мне нужно создать словарь, в котором ключи - это те элементы из второго списка, которые находятся в первом, а значения - это списки элементов, найденных между "ключами", например:

result = {
    'a': ['el1', 'el2', 'el3', 'el4'],
    'b': ['some_other_el_1', 'some_other_el_2'],
    'c': ['another_element_1', 'another_element_2'],
    'd': ['', '', 'another_element_3', 'd4']
}

Какой более питонический способ сделать это?

В настоящее время я делаю это:

# I'm not sure that the first element in the second list
# will also be in the first so I have to create a key
k = ''
d[k] = []
for x in l2:
    if x in l1:
        k = x
        d[k] = []
    else:
        d[k].append(x)

Но я совершенно уверен, что это не лучший способ сделать это, и это также не выглядит красиво :)

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


person Cristian Harangus    schedule 15.05.2018    source источник
comment
Всегда ли l2 сортируется?   -  person zwer    schedule 15.05.2018
comment
И будут ли другие элементы l2 всегда начинаться с элемента l1 (то есть a в a1.)   -  person Roelant    schedule 15.05.2018
comment
@zwer ... нет. Это список, в который я просто добавляю некоторые элементы, чтобы он не сортировался.   -  person Cristian Harangus    schedule 15.05.2018
comment
@Roelant ... не всегда, или, по крайней мере, я не могу быть в этом уверен.   -  person Cristian Harangus    schedule 15.05.2018
comment
@CristianHarangus. Так что же происходит с элементами если l2 не начинается с элемента из l1?   -  person zwer    schedule 15.05.2018
comment
имеет ли значение порядок в значениях результирующего словаря?   -  person Ma0    schedule 15.05.2018
comment
@zwer, тогда у меня будет пустой словарь или все элементы будут в списке с ключом, который, как я знаю, является ключом по умолчанию (поэтому я не буду использовать его позже)   -  person Cristian Harangus    schedule 15.05.2018
comment
@ Ev.Kounis ... порядок результата не имеет значения.   -  person Cristian Harangus    schedule 15.05.2018


Ответы (8)


Я не думаю, что вы добьетесь большего успеха, если это будет наиболее конкретная постановка задачи. То есть я бы сделал так, но это ненамного лучше.

import collections


d = collections.defaultdict(list)
s = set(l1)
k = ''

for x in l2:
    if x in s:
        k = x
    else:
        d[k].append(x)
person FHTMitchell    schedule 15.05.2018

Ради интереса вы также можете сделать это с помощью itertools и стороннего numpy:

import numpy as np
from itertools import zip_longest, islice

arr = np.where(np.in1d(l2, l1))[0]
res = {l2[i]: l2[i+1: j] for i, j in zip_longest(arr, islice(arr, 1, None))}

print(res)

{'a': ['el1', 'el2', 'el3', 'el4'],
 'b': ['some_other_el_1', 'some_other_el_2'],
 'c': ['another_element_1', 'another_element_2'],
 'd': ['', '', 'another_element_3', 'd4']}
person jpp    schedule 15.05.2018

Вот версия с использованием itertools.groupby. Это может быть или не быть более эффективным, чем обычная версия из вашего поста, в зависимости от того, как реализован groupby, потому что цикл for имеет меньше итераций.

from itertools import groupby
from collections import defaultdict, deque

def group_by_keys(keys, values):
    """
    >>> sorted(group_by_keys('abcdef', [
    ...          1, 2, 3,
    ...     'b', 4, 5,
    ...     'd',
    ...     'a', 6, 7,
    ...     'c', 8, 9,
    ...     'a', 10, 11, 12
    ... ]).items())
    [('a', [6, 7, 10, 11, 12]), ('b', [4, 5]), ('c', [8, 9])]
    """
    keys = set(keys)
    result = defaultdict(list)
    current_key = None
    for is_key, items in groupby(values, key=lambda x: x in keys):
        if is_key:
            current_key = deque(items, maxlen=1).pop()  # last of items
        elif current_key is not None:
            result[current_key].extend(items)
    return result

Это не делает различий между ключами, которые вообще не встречаются в values (например, e и f), и ключами, для которых нет соответствующих значений (например, d). Если эта информация необходима, возможно, лучше подойдет одно из других решений.

person mkrieger1    schedule 15.05.2018
comment
В этом решении есть ошибка: если у вас есть последовательность из более чем одного элемента, появляющегося в l1, то в результате появляется только первый элемент, а остальные элементы отбрасываются. Вместо этого я ожидал найти все эти элементы в виде отдельных ключей, но, возможно, с пустыми списками в качестве значений. Для этого у вас может быть счетчик nonlocal, тогда функция key становится чем-то вроде counter += x in keys; return counter. - person Bakuriu; 15.05.2018
comment
@Bakuriu Я не уверен, что понимаю, что ты имеешь в виду. Можете ли вы показать пример? - person mkrieger1; 15.05.2018
comment
Попробуйте свою функцию на чем-то вроде group_by_keys('abcdef', ['a', 'b', 1, 2, 3]). Результат {'a': [1,2,3]} вместо ожидаемого {'a': [], 'b': [1,2,3]} - person Bakuriu; 15.05.2018

Обновлено... Снова

Я неверно истолковал вопрос. Если вы используете большие списки, то понимание списков — это то, что вам нужно, и они довольно просты, как только вы научитесь их использовать.

Я собираюсь использовать два понимания списка.

idxs = [i for i, val in enumerate(l2) if val in l1] + [len(l2)+1]
res = {l2[idxs[i]]: list(l2[idxs[i]+1: idxs[i+1]]) for i in range(len(idxs)-1)}
print(res)

Полученные результаты:

{'a': ['el1', 'el2', 'el3', 'el4'],
 'b': ['some_other_el_1', 'some_other_el_2'],
 'c': ['another_element_1', 'another_element_2'],
 'd': ['', '', 'another_element_3', 'd4']}

Тестирование скорости для больших списков:

import collections


l1 = ['a', 'c', 'b', 'e', 'f', 'd']
l2 = [
    'x','q','we','da','po',
    'a', 'el1', 'el2', 'el3', 'el4', *(str(i) for i in range(300)),
    'b', 'some_other_el_1', 'some_other_el_2', *(str(i) for i in range(100)),
    'c', 'another_element_1', 'another_element_2', *(str(i) for i in range(200)),
    'd', '', '', 'another_element_3', 'd4'
]


def run_comp():
    idxs = [i for i, val in enumerate(l2) if val in l1] + [len(l2)+1]
    res = {l2[idxs[i]]: list(l2[idxs[i]+1: idxs[i+1]]) for i in range(len(idxs)-1)}


def run_other():
    d = collections.defaultdict(list)
    k = ''

    for x in l2:
        if x in l1:
            k = x
        else:
            d[k].append(x)


import timeit
print('For Loop:', timeit.timeit(run_other, number=1000))
print("List Comprehension:", timeit.timeit(run_comp, number=1000))

Полученные результаты:

For Loop: 0.1327093063242541
List Comprehension: 0.09343156142774986

старые материалы ниже

Это довольно просто со списками.

{key: [val for val in l2 if key in val] for key in l1}

Полученные результаты:

{'a': ['a', 'a1', 'a2', 'a3', 'a4'],
 'b': ['b', 'b1', 'b2', 'b3', 'b4'],
 'c': ['c', 'c1', 'c2', 'c3', 'c4'],
 'd': ['d', 'd1', 'd2', 'd3', 'd4'],
 'e': [],
 'f': []}

Код ниже показывает, что происходит выше.

d = {}
for key in l1:
    d[key] = []
    for val in l2:
        if key in val:
            d[key].append(val)

Понимание списка/понимание словаря (первая часть кода) на самом деле намного быстрее. Понимание списков создает список на месте, что намного быстрее, чем просмотр и добавление к списку. Добавление заставляет программу проходить по списку, выделять больше памяти и добавлять данные в список, что может быть очень медленным для больших списков.

Использованная литература:

person justengel    schedule 15.05.2018
comment
это не то, что хочет сделать ОП .. Это просто выглядит так, потому что он использует плохой пример. ключи — это те элементы из второго списка, которые находятся в первом, а значения — это списки элементов, найденных между ключами - person Ma0; 15.05.2018
comment
За исключением того, что это не намного быстрее, на самом деле это будет медленнее, чем решение OP, поскольку вы заменили проблему O(n) на задачу O(n²). Не говоря уже о том, что он не выполняет то, о чем просит ОП. - person zwer; 15.05.2018
comment
@ Ev.Kounis ... да ... из-за того, что я здесь новичок, я опубликовал плохой пример. Отредактировано позже. Надеюсь теперь будет понятнее - person Cristian Harangus; 15.05.2018
comment
@zwer - Если вы собираетесь перейти к большой нотации O, вы также должны учитывать тот факт, что добавление списка делает с Python. С каждым добавлением вы просматриваете список и выделяете память. Выделение памяти занимает много времени и ресурсов. Я провел много тестов скорости с помощью Python для приложений реального времени. Использование цикла for и добавление к списку в python ужасно медленно. - person justengel; 15.05.2018
comment
@Christian публикует пример, когда правильные ключи не всегда случайно совпадают с первой буквой всех их значений.. - person Ma0; 15.05.2018
comment
@ Ev.Kounis извините за неправильное толкование вопроса. Я обновил свой ответ, используя 2 понимания списка. - person justengel; 15.05.2018
comment
Технически второе понимание списка на самом деле является пониманием словаря. Но +1, логика почти идентична моему решению numpy. - person jpp; 15.05.2018

Вы можете использовать itertools.groupby:

import itertools
l1 = ['a', 'c', 'b', 'e', 'f', 'd']
l2 = ['x', 'q', 'we', 'da', 'po', 'a', 'el1', 'el2', 'el3', 'el4', 'b', 'some_other_el_1', 'some_other_el_2', 'c', 'another_element_1', 'another_element_2', 'd', '', '', 'another_element_3', 'd4']
groups = [[a, list(b)] for a, b in itertools.groupby(l2, key=lambda x:x in l1)]
final_dict = {groups[i][-1][-1]:groups[i+1][-1] for i in range(len(groups)-1) if groups[i][0]}

Выход:

{'a': ['el1', 'el2', 'el3', 'el4'], 'b': ['some_other_el_1', 'some_other_el_2'], 'c': ['another_element_1', 'another_element_2'], 'd': ['', '', 'another_element_3', 'd4']}
person Ajax1234    schedule 15.05.2018
comment
Даже если он работает нормально, какой код вы бы предпочли поддерживать? Твой или ОП? - person Eric Duminil; 16.05.2018

Ваш код удобочитаем, выполняет свою работу и достаточно эффективен. Не нужно много менять!

Вы можете использовать более описательные имена переменных и заменить l1 набором для более быстрый поиск:

keys = ('a', 'c', 'b', 'e', 'f', 'd')
keys_and_values = [
    'x','q','we','da','po',
    'a', 'el1', 'el2', 'el3', 'el4',
    'b', 'some_other_el_1', 'some_other_el_2',
    'c', 'another_element_1', 'another_element_2',
    'd', '', '', 'another_element_3', 'd4'
]

current_key = None
result = {}
for x in keys_and_values:
    if x in keys:
        current_key = x
        result[current_key] = []
    elif current_key:
        result[current_key].append(x)

print(result)
# {'a': ['el1', 'el2', 'el3', 'el4'],
#  'c': ['another_element_1', 'another_element_2'],
#  'b': ['some_other_el_1', 'some_other_el_2'],
#  'd': ['', '', 'another_element_3', 'd4']}
person Eric Duminil    schedule 15.05.2018
comment
Downvoter: Буду рад услышать конструктивную критику! - person Eric Duminil; 17.05.2018

 def find_index():
    idxs = [l2.index(i) for i in set(l1).intersection(set(l2))]
    idxs.sort()
    idxs+= [len(l2)+1]
    res = {l2[idxs[i]]: list(l2[idxs[i]+1: idxs[i+1]]) for i in range(len(idxs)-1)}
    return(res)

Сравнение методов с использованием теста Justengel:
justengel
run_comp: .455
run_other: .244
mkrieger1
group_by_keys: . 160
я
find_index: .068

Обратите внимание, что мой метод игнорирует ключи, которые не появляются l2, и не обрабатывает случаи, когда ключи появляются более одного раза в l2. Добавление в пустые списки ключей, которых нет в l2, может быть выполнено {**res, **{key: [] for key in set(l1).difference(set(l2))}}, что увеличивает время до 0,105.

person Acccumulation    schedule 15.05.2018

Даже чище, чем превращение l1 в set, используйте ключи словаря, который вы создаете. Как это

d = {x: [] for x in l1}
k = None

for x in l2:
    if x in d:
        k = x
    elif k is not None:
        d[k].append(x)

Это связано с тем, что (в худшем случае) ваш код будет перебирать все значения в l1 для каждого значения в l2 в строке if x in l1:, потому что проверка того, является ли значение in списком, занимает линейное время. Проверка того, является ли значение in ключами словаря, в среднем является постоянным временем (то же самое с sets, как уже предложил Эрик Думинил).

Я установил k в None и проверил его, потому что ваш код вернул бы d с '': ['x','q','we','da','po'], что, по-видимому, не то, что вам нужно. Это предполагает, что l1 не может содержать None.

Мое решение также предполагает, что результирующий словарь может содержать ключи с пустыми списками, если в l1 есть элементы, которые никогда не появляются в l2. Если это не нормально, вы можете удалить их в конце с помощью

final_d = {k: v for k, v in d.items() if v}
person Boris    schedule 11.06.2018