Search code examples
pythonpython-2.7counterordereddictionary

Get key count from OrderedDict where key is a tuple


I've a dictionary such as this:

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

The combination of the tuple is always unique and I would like to maintain the order of insertion, and hence OrderedDict. I've a well over ~10K items in the dict. How can I efficiently maintain a counter that gives the count of the second element in the tuple? Basically, I need to know the count whenever I would like to add/delete an item in the key. Right now I just iterate through my_dict and get the counter everytime but it seems to be very expensive to do that.

In the above example I want the output to be:

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

Right now I do the following:

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)

I'm not sure if this is the best way but is there a way to override the the = operator and pop function, such that it adds or subtracts a count every time I do that operation?


Solution

  • Getting a Counter to work nicely with an OrderedDict is probably going to require some subclassing. Here's something that might work (I've only implemented __setitem__ and __getitem__, but if you'd like a more robust implementation, let me know):

    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)
    

    Example usage:

    >>> 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})
    

    Here's a more general CountedOrderedDict implementation that takes a key function as a parameter.

    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)
    

    For your needs, you'd instantiate it like so:

    my_dict = CountedOrderedDict(key=lambda k: k[1])