2012-03-28 4 views
3

Этот вопрос задан this question. Я хотел бы получить словарь из списка словарей, который должен содержать все пары ключ/значение из всех словарей, которые либо содержатся только один раз, либо где все словари согласуются с соответствующим значением. Пример (из вышеупомянутой публикации):Создайте словарь непротиворечивых предметов из списка словарей

dicts = [dict(a=3, b=89, d=2), dict(a=3, b=89, c=99), dict(a=3, b=42, c=33)] 
print dict_itersection(dicts) 

должны давать

{'a': 3, 'd': 2} 

Моя текущая реализация выглядит следующим образом:

import collections 

def dict_intersection(dicts): 
     c=collections.defaultdict(set) 
     for d in dicts: 
       for a, b in d.iteritems(): 
         c[a].add(b) 
     return {a: next(iter(b)) for a, b in c.iteritems() if len(b) == 1} 

Так что мой вопрос: Можно ли это сделать элегантнее ?

Sidequestion: может next(iter(b)) быть сделано лучше без модификации базового словаря (т.е. не b.pop())?

+2

Все, что нужно, это два комментария. 'b.pop()' будет здесь отлично, поскольку он только изменяет ваши новые временные множества. Нет другого способа получить один элемент из набора. потому что у наборов нет порядка и, следовательно, нет 'myset [0]' –

ответ

3

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

def dict_intersection(dicts): 
    result = {} 
    conflicting = set() 
    for d in dicts: 
     for k, v in d.iteritems(): 
      if k not in conflicting and result.setdefault(k, v) != v: 
       del result[k] 
       conflicting.add(k) 
    return result 

Набор conflicting будет содержать только словарные ключи, которые всегда будут hashable.

+0

Это действительно просто и работает для случаев, когда у всех других решений нет, поэтому я соглашусь с этим. – hochl

4

У вас довольно близкий и элегантный, как я могу себе представить. Единственное изменение, которое я хотел бы сделать, чтобы заменить вложенный цикл с itertools.chain() «эд итератора, как это:

import collections 

def dict_intersection(dicts): 
     c=collections.defaultdict(set) 
     for k,v in itertools.chain(*[d.iteritems() for d in dicts]): 
       c[k].add(v) 
     return {a: next(iter(b)) for a, b in c.iteritems() if len(b) == 1} 

Edit (1): ниже код отвечает немного другой вопрос - как получить любая запись, которая появляется с одним и тем же ключом и значением по меньшей мере в двух входных словарях.

Моего ответа от комментариев в другом вопросе:

dict(
    [k for k,count in 
    collections.Counter(itertools.chain(*[d.iteritems() for d in dicts])).iteritems() 
    if count > 1] 
    ) 

Это номинально «один вкладыш», но я развернул его на несколько строк, чтобы (я надеюсь) сделать это немного яснее.

Путь это работает (начиная с внутренней и разработка):

  • Используйте itertools.chain(), чтобы получить итератор над элементами всех словарей.
  • Использовать collections.Counter(), чтобы подсчитать, сколько раз каждая key, value пара появляется в словарях.
  • Используйте список для фильтрации Counter для тех, кто key, value пары встречаются не менее двух раз.
  • Преобразуйте список обратно в dict.
+0

Это печатает '{'a': 3, 'b': 89}' для моего примера ... – hochl

+0

А, вы слегка изменили вопрос. Всего секунду ... –

4
dicts = [dict(a=3, b=89, d=2), dict(a=3, b=89, c=99), dict(a=3, b=42, c=33)] 

data = {} 
for d in dicts: 
    for k, v in d.iteritems(): 
     data.setdefault(k, set()).add(v) 
out = dict((k, v.pop()) for k, v in data.iteritems() if len(v) == 1) 

# out == {'a': 3, 'd': 2} 

... или один вкладыш:

import itertools as it 

dict((k, v.pop()[1]) for k,v in ((k, set(v)) for k, v in it.groupby(sorted(it.chain(*(d.iteritems() for d in dicts))), key=lambda x: x[0])) if len(v) == 1) 
+0

Wut ............ –

+0

Neato! Также можно использовать 'key = operator.itemgetter (0)'. Обратите внимание, что в Py3k это более аккуратно, так как вы можете использовать dict и установить либералов, и вам не нужно вызывать '.iteritems()'. – katrielalex

1

Чтобы получить пересечение:

dict(reduce(lambda x, y: x & y, map(set, map(lambda x: x.iteritems(), dicts)))) 

Конечно, это капли уникальных значений, так что нам нужно, чтобы получить дополнение :

dict(reduce(lambda x, y: x - y, map(set, map(lambda x: x.iteritems(), dicts)))) 

Объединение полученных словарей дает нам результирующий набор:

def dict_intersection(d): 
    x = dict(reduce(lambda x, y: x & y, map(set, map(lambda x: x.iteritems(), dicts)))) 
    y = dict(reduce(lambda x, y: x - y, map(set, map(lambda x: x.iteritems(), dicts)))) 
    return dict(x.items() + y.items()) 

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

+0

К сожалению, это не будет выполнено для '[{'a': 3, 'b': 89, 'd': 2}, {'a': 3, 'c': 99, 'b': 89}, {' a ': 3,' c ': 33,' b ': 42}, {' x ': 5}] '(хотя мне очень нравится идея множества операций). Кроме того, я думаю, вы можете сжать свои утверждения в 'tmp = [set (d.iteritems()) для d в dicts]; return dict (set.intersection (* tmp) .union (set.difference (* tmp))) ' – hochl

Смежные вопросы