Получить количество ключей из OrderedDict, где ключ является кортежем

У меня есть такой словарь:

my_dict=collections.OrderedDict([((123, 1), 'qwe'), ((232, 1), 'asd'), ((234, 2), 'zxc'), ((6745, 2), 'aaa'), ((456, 3), 'bbb')])

Комбинация кортежа всегда уникальна, и я хотел бы сохранить порядок вставки и, следовательно, OrderedDict. У меня более ~ 10 тыс. элементов в dict. Как я могу эффективно поддерживать счетчик, который дает количество второго элемента в кортеже? По сути, мне нужно знать количество всякий раз, когда я хочу добавить/удалить элемент в ключе. Прямо сейчас я просто перебираю my_dict и получаю счетчик каждый раз, но это кажется очень дорогим.

В приведенном выше примере я хочу, чтобы вывод был:

1:2 # As in 1 occurs 2 times 
2:2
3:1

Прямо сейчас я делаю следующее:

from collections import OrderedDict, Counter
my_dict = OrderedDict()
my_dict[(123,1)] = 'qwe'
my_dict[(232,1)] = 'asd'
my_dict[(234,2)] = 'zxc'
my_dict[(6745,2)] = 'aaa'
my_dict[(456,3)] = 'bbb'
cnt = []
for item in my_dict.keys():
    cnt.append(item[1])
print Counter(cnt)

Я не уверен, что это лучший способ, но есть ли способ переопределить оператор = и функцию pop, чтобы он добавлял или вычитал счетчик каждый раз, когда я выполняю эту операцию?


person 0x0    schedule 28.08.2014    source источник
comment
Вам, вероятно, лучше всего подойдет пользовательский класс, реализующий __setitem__ и сохраняющий экземпляры Counter и OrderedDict в качестве базовых атрибутов.   -  person g.d.d.c    schedule 28.08.2014
comment
Первая строка не имеет никакого эффекта. my_dict присваивается обычному dict во второй строке.   -  person jfs    schedule 28.08.2014
comment
@ J.F.Sebastian Вы правы. Я не думал. Исправил свои примеры.   -  person 0x0    schedule 28.08.2014
comment
collections.Counter(x[1] for x in my_dict.iterkeys()) будет делать то, что делает ваш цикл for   -  person Padraic Cunningham    schedule 28.08.2014


Ответы (1)


Чтобы Counter хорошо работал с OrderedDict, вероятно, потребуется некоторый подкласс. Вот что может сработать (я реализовал только __setitem__ и __getitem__, но если вам нужна более надежная реализация, дайте мне знать):

import collections

class CountedOrderedDict(collections.OrderedDict):
    def __init__(self, *args, **kwargs):
        self.counter = collections.Counter()
        super(CountedOrderedDict, self).__init__(*args, **kwargs)

    def __delitem__(self, key):
        super(CountedOrderedDict, self).__delitem__(key)
        self.counter[key[1]] -= 1

    def __setitem__(self, key, value):
        if key not in self:
            self.counter[key[1]] += 1

        super(CountedOrderedDict, self).__setitem__(key, value)

Пример использования:

>>> my_dict = CountedOrderedDict({(123,1): 'sda', (232,1) : 'bfd', (234,2) : 'csd', (6745,2) : 'ds', (456,3) : 'rd'})
>>> my_dict.counter
Counter({'1': 2, '2': 2, '3': 1})
>>> del my_dict[(123,1)]
>>> my_dict.counter
Counter({'2': 2, '1': 1, '3': 1})
>>> my_dict[(150,1)] = "asdf"
>>> my_dict.counter
Counter({'1': 2, '2': 2, '3': 1})

Вот более общая реализация CountedOrderedDict, которая принимает ключевую функцию в качестве параметра.

import collections

class CountedOrderedDict(collections.OrderedDict):
    def __init__(self, key=lambda k: k, *args, **kwargs):
        self.counter = collections.Counter()
        self.key_transform = key
        super(CountedOrderedDict, self).__init__(*args, **kwargs)

    def __delitem__(self, key):
        super(CountedOrderedDict, self).__delitem__(key)
        self.counter[self.key_transform(key)] -= 1

    def __setitem__(self, key, value):
        if key not in self:
            self.counter[self.key_transform(key)] += 1

        super(CountedOrderedDict, self).__setitem__(key, value)

Для ваших нужд вы должны создать его так:

my_dict = CountedOrderedDict(key=lambda k: k[1])
person dwlz    schedule 28.08.2014
comment
В обоих классах я бы предложил, чтобы __delitem__ повторно вызывал исключение, которое он перехватывает, а не подавлял его. Простейшим способом сделать это может быть просто запись вызова super и декремента без каких-либо блоков try/except. Любое исключение, вызванное в super().__delitem__, остановит декремент! В методе __init__ второго класса в Python 3 вы, вероятно, захотите сделать key аргументом, состоящим только из ключевого слова, переместив его после *args. Таким образом, вы можете фактически передавать позиционные аргументы, причем первый из них не обязательно должен быть key. Я бы также предложил использовать имя, отличное от key! - person Blckknght; 28.08.2014
comment
@Blckknght Хорошее предложение. Я писал сложный и уродливый комментарий, чтобы задать ту же проблему, за исключением. Спасибо! - person 0x0; 28.08.2014
comment
Я изменил порядок ключей в параметрах, чтобы аргументы, не являющиеся ключевыми словами, можно было указывать без указания ключа, но оставил ключ в качестве имени параметра, поскольку он используется аналогичным образом в других случаях (например, в sorted и max/min). - person dwlz; 28.08.2014
comment
@Dan, компилируя это, дает мне недопустимую синтаксическую ошибку в строке def __init__(self, *args, key=lambda k: k, **kwargs). Не понимаю, зачем предлагать. - person 0x0; 28.08.2014
comment
Да, вот что происходит, когда я редактирую код, не запуская его сначала. :) Исправлено. - person dwlz; 28.08.2014
comment
Как вы думаете, вы знаете, почему это происходит? Согласно @Blckknght, похоже, что перемещение key после *args делает его аргументом только для ключевого слова (не знаю, как), но интуитивно это не должно вызывать ошибку, верно? Спасибо большое. - person 0x0; 28.08.2014
comment
@Сунил Хороший вопрос. Этот синтаксис будет работать в Python 3, но не будет работать с Python 2.7 или ниже. Я просто изменил его на то, что у меня было изначально. - person dwlz; 28.08.2014