added multi_key_dictionary
This commit is contained in:
		
							
								
								
									
										279
									
								
								multi_key_dictionary.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								multi_key_dictionary.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,279 @@
 | 
			
		||||
'''
 | 
			
		||||
Created on 26 May 2013
 | 
			
		||||
 | 
			
		||||
@author: lukasz.forynski
 | 
			
		||||
 | 
			
		||||
@brief: Implementation of the multi-key dictionary.
 | 
			
		||||
___________________________________
 | 
			
		||||
 | 
			
		||||
 Copyright (c) 2013 Lukasz Forynski <lukasz.forynski@gmail.com>
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
 | 
			
		||||
software and associated documentation files (the "Software"), to deal in the Software
 | 
			
		||||
without restriction, including without limitation the rights to use, copy, modify, merge,
 | 
			
		||||
publish, distribute, sub-license, and/or sell copies of the Software, and to permit persons
 | 
			
		||||
to whom the Software is furnished to do so, subject to the following conditions:
 | 
			
		||||
 | 
			
		||||
- The above copyright notice and this permission notice shall be included in all copies
 | 
			
		||||
or substantial portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 | 
			
		||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 | 
			
		||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 | 
			
		||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 | 
			
		||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 | 
			
		||||
DEALINGS IN THE SOFTWARE.
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
class multi_key_dictionary(object):
 | 
			
		||||
    """ Purpose of this type is to provie a multi-key dictionary.
 | 
			
		||||
    Such a dictionary has the same interface as a standard dictionary, but allows
 | 
			
		||||
    accessing / iterating items using multiple keys, i.e. it provices mapping from
 | 
			
		||||
    different types of keys (and also different keys of the same type) to the same value. 
 | 
			
		||||
    For example:
 | 
			
		||||
    
 | 
			
		||||
        k = multi_key_dictionary()
 | 
			
		||||
        k[100] = 'hundred'  # add item to the dictionary (as for normal dictionary)
 | 
			
		||||
        
 | 
			
		||||
        # but also:
 | 
			
		||||
        # below creates entry with two possible key types: int and str, 
 | 
			
		||||
        # mapping all keys to the assigned value
 | 
			
		||||
        k[1000, 'kilo', 'k'] = 'kilo (x1000)'
 | 
			
		||||
        print k[100]   # will print 'kilo (x1000)'
 | 
			
		||||
        print k['k']  # will also print 'okilo (x1000)'
 | 
			
		||||
        
 | 
			
		||||
        # the same way objects can be updated, and if an object is updated using one key, the new value will
 | 
			
		||||
        # be accessible using any other key, e.g. for example above:
 | 
			
		||||
        k['kilo'] = 'kilo'
 | 
			
		||||
        print k[1000] # will print 'kilo' as value was updated
 | 
			
		||||
    """
 | 
			
		||||
    def __getitem__(self, key):
 | 
			
		||||
        """ Return the value at index specified as key."""
 | 
			
		||||
        key_type = str(type(key))
 | 
			
		||||
        if (self.__dict__.has_key(key_type) and
 | 
			
		||||
            self.__dict__[key_type].has_key(key)
 | 
			
		||||
#             and self.items_dict and
 | 
			
		||||
#             self.items_dict.has_key(self.__dict__[key_type][key])
 | 
			
		||||
            ):
 | 
			
		||||
            return self.items_dict[self.__dict__[key_type][key]]
 | 
			
		||||
        else:
 | 
			
		||||
            raise KeyError(key)
 | 
			
		||||
 | 
			
		||||
    def __setitem__(self, keys, value):
 | 
			
		||||
        """ Set the value at index (or list of indexes) specified as keys."""
 | 
			
		||||
        if(type(keys) == type(tuple())):
 | 
			
		||||
            first_key = keys[0]  # if it's a list, just use the first item
 | 
			
		||||
        else:
 | 
			
		||||
            first_key = keys
 | 
			
		||||
        key_type = str(type(first_key)) # find the intermediate dictionary..
 | 
			
		||||
        if self.__dict__.has_key(key_type) and self.__dict__[key_type].has_key(first_key):
 | 
			
		||||
            self.items_dict[self.__dict__[key_type][first_key]] = value # .. and update the object if it exists..
 | 
			
		||||
        else:
 | 
			
		||||
            if(type(keys) != type(tuple())):
 | 
			
		||||
                key = keys
 | 
			
		||||
                keys = [keys]
 | 
			
		||||
            self.__add_item(value, keys) # .. or create it - if it doesn't
 | 
			
		||||
 | 
			
		||||
    def __delitem__(self, key):
 | 
			
		||||
        """ Called to implement deletion of self[key]."""
 | 
			
		||||
        key_type = str(type(key))
 | 
			
		||||
        if (self.__dict__.has_key(key_type) and
 | 
			
		||||
            self.__dict__[key_type].has_key(key) and
 | 
			
		||||
            self.items_dict and
 | 
			
		||||
            self.items_dict.has_key(self.__dict__[key_type][key])):
 | 
			
		||||
            intermediate_key = self.__dict__[key_type][key]
 | 
			
		||||
 | 
			
		||||
            # remove the item in main dictionary 
 | 
			
		||||
            del self.items_dict[intermediate_key]
 | 
			
		||||
 | 
			
		||||
            # remove all references (also pointed by other types of keys)
 | 
			
		||||
            # for the item that this key pointed to.
 | 
			
		||||
            for name, reference_dict in self.__dict__.iteritems():
 | 
			
		||||
                if(type(name) == type(str()) and name.find('<type') == 0):
 | 
			
		||||
                    ref_key = None
 | 
			
		||||
                    for temp_key, value in reference_dict.iteritems():
 | 
			
		||||
                        if value == intermediate_key:
 | 
			
		||||
                            ref_key = temp_key
 | 
			
		||||
                            break
 | 
			
		||||
                    if ref_key:
 | 
			
		||||
                        del reference_dict[ref_key]
 | 
			
		||||
        else:
 | 
			
		||||
            raise KeyError(key)
 | 
			
		||||
 | 
			
		||||
    def iteritems(self, by_key=None):
 | 
			
		||||
        """ Returns an iterator over the dictionary's (key, value) pairs.
 | 
			
		||||
            @param by_key if specified, iterator for a dictionary of it's type will be used (if available).
 | 
			
		||||
                   Otherwise, iterator will use a dictionary for which keys are first in lexicographical order.
 | 
			
		||||
        """
 | 
			
		||||
        intermediate_key = self.__find_intermediate_key(by_key)
 | 
			
		||||
        if intermediate_key and self.__dict__.has_key(intermediate_key):
 | 
			
		||||
            for key, direct_key in self.__dict__[intermediate_key].iteritems():
 | 
			
		||||
                yield key, self.items_dict[direct_key]
 | 
			
		||||
                
 | 
			
		||||
    def iterkeys(self, by_key=None):
 | 
			
		||||
        """ Returns an iterator over the dictionary's keys.
 | 
			
		||||
            @param by_key if specified, iterator for a dictionary of it's type will be used (if available).
 | 
			
		||||
                   Otherwise, iterator will use a dictionary for which keys are first in lexicographical order.
 | 
			
		||||
        """
 | 
			
		||||
        intermediate_key = self.__find_intermediate_key(by_key)
 | 
			
		||||
        if intermediate_key and self.__dict__.has_key(intermediate_key):
 | 
			
		||||
            return self.__dict__[intermediate_key].iterkeys()
 | 
			
		||||
 | 
			
		||||
    def itervalues(self, by_key=None):
 | 
			
		||||
        """ Returns an iterator over the dictionary's values.
 | 
			
		||||
            @param by_key if specified, iterator for a dictionary of it's type will be used (if available).
 | 
			
		||||
                   Otherwise, iterator will use a dictionary for which keys are first in lexicographical order.
 | 
			
		||||
        """
 | 
			
		||||
        intermediate_key = self.__find_intermediate_key(by_key)        
 | 
			
		||||
        if intermediate_key and self.__dict__.has_key(intermediate_key):
 | 
			
		||||
            for direct_key in self.__dict__[intermediate_key].itervalues():
 | 
			
		||||
                yield self.items_dict[direct_key]
 | 
			
		||||
        
 | 
			
		||||
    def items(self, by_key=None):
 | 
			
		||||
        """ Returns a copy of the dictionary's values.
 | 
			
		||||
            @param by_key if specified, values will be sorted in the order for dictionary of it's type.
 | 
			
		||||
                 Otherwise they will be sorted by in the order for dictionary for which type of it's key
 | 
			
		||||
                 is first in lexicographical order (i.e.: as for '(type(xx))'
 | 
			
		||||
        """
 | 
			
		||||
        all_items = []
 | 
			
		||||
        intermediate_key = self.__find_intermediate_key(by_key)
 | 
			
		||||
        if intermediate_key and self.__dict__.has_key(intermediate_key):
 | 
			
		||||
            for direct_key in self.__dict__[intermediate_key].itervalues():
 | 
			
		||||
                all_items.append(self.items_dict[direct_key])
 | 
			
		||||
        return all_items
 | 
			
		||||
 | 
			
		||||
    def keys(self, by_key=None):
 | 
			
		||||
        """ Returns a copy of the dictionary's keys.
 | 
			
		||||
            @param by_key if specified, keys will be sorted in the order for dictionary of it's type.
 | 
			
		||||
                 Otherwise they will be sorted by in the order for dictionary for which type of it's key
 | 
			
		||||
                 is first in lexicographical order (i.e.: as for '(type(xx))'
 | 
			
		||||
        """
 | 
			
		||||
        intermediate_key = self.__find_intermediate_key(by_key)
 | 
			
		||||
        if intermediate_key and self.__dict__.has_key(intermediate_key):
 | 
			
		||||
            return self.__dict__[intermediate_key].keys()
 | 
			
		||||
 | 
			
		||||
    def __find_intermediate_key(self, by_key=None):
 | 
			
		||||
        """ Internal method to find the intermediate key for a requested type"""
 | 
			
		||||
        intermediate_key = None        
 | 
			
		||||
        if(by_key is not None):
 | 
			
		||||
            intermediate_key = str(type(by_key))
 | 
			
		||||
        else:
 | 
			
		||||
            for attr in self.__dict__.iteritems():
 | 
			
		||||
                if(type(attr[0]) == type(str()) and attr[0].find('<type') == 0):
 | 
			
		||||
                    intermediate_key = attr[0] # just use the first one available
 | 
			
		||||
                    break
 | 
			
		||||
        return intermediate_key
 | 
			
		||||
 | 
			
		||||
    def __add_item(self, item, keys=None):        
 | 
			
		||||
        """ Internal method to add an item to the multi-key dictionary"""
 | 
			
		||||
        if(not keys or not len(keys)):
 | 
			
		||||
            raise Exception('Error in %s.__add_item(%s, keys=tuple/list of items): need to specify a tuple/list containing at least one key!'
 | 
			
		||||
                            % (self.__class__.__name__, str(item)))
 | 
			
		||||
        direct_key = '_'.join([str(key) for key in keys]) # joined values of keys will be used as a direct key        
 | 
			
		||||
        for key in keys:
 | 
			
		||||
            key_type = str(type(key))
 | 
			
		||||
 | 
			
		||||
            # store direct key as a value in an intermediate dictionary
 | 
			
		||||
            if(not self.__dict__.has_key(key_type)):
 | 
			
		||||
                self.__setattr__(key_type, dict())
 | 
			
		||||
            self.__dict__[key_type][key] = direct_key
 | 
			
		||||
         
 | 
			
		||||
            # store the value in the actual dictionary
 | 
			
		||||
            if(not self.__dict__.has_key('items_dict')):
 | 
			
		||||
                self.items_dict = dict()            
 | 
			
		||||
            self.items_dict[direct_key] = item
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_multintermediate_key_dictionary():    
 | 
			
		||||
    m = multi_key_dictionary()
 | 
			
		||||
    m['aa', 12] = 123  # create a value with multiple keys..
 | 
			
		||||
 | 
			
		||||
    m['something else'] = 'abcd'
 | 
			
		||||
    m[23] = 0
 | 
			
		||||
    
 | 
			
		||||
    # check if it's possible to read this value back using either of keys
 | 
			
		||||
    assert( m['aa'] == 123 ), 'expected m[\'aa\'] == 123'
 | 
			
		||||
    assert( m[12] == 123 ), 'expected m[12] == 123'
 | 
			
		||||
    
 | 
			
		||||
    # now update value and again - confirm it back - using different keys..
 | 
			
		||||
    m['aa'] = 45
 | 
			
		||||
    assert( m['aa'] == 45 ), 'expected m[\'aa\'] == 45'
 | 
			
		||||
    assert( m[12] == 45 ), 'expected m[12] == 45'
 | 
			
		||||
    
 | 
			
		||||
    m[12] = 4
 | 
			
		||||
    assert( m['aa'] == 4 ), 'expected m[\'aa\'] == 4'
 | 
			
		||||
    assert( m[12] == 4 ), 'expected m[12] == 4'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    # now test deletion..
 | 
			
		||||
    del m[12]
 | 
			
		||||
 | 
			
		||||
    # try removing again 
 | 
			
		||||
    try:
 | 
			
		||||
        del m['aa']
 | 
			
		||||
        assert(False), 'cant remove again: item m[\'aa\'] should not exist!'
 | 
			
		||||
    except KeyError, err:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    # above doesn't exist 
 | 
			
		||||
    try:
 | 
			
		||||
        k =  m[12]
 | 
			
		||||
        assert(False), 'removed item m[12] should exist!'
 | 
			
		||||
    except KeyError, err:
 | 
			
		||||
        pass
 | 
			
		||||
 
 | 
			
		||||
    # and also for other keys.. 
 | 
			
		||||
    try:
 | 
			
		||||
        k =  m['aa']
 | 
			
		||||
        assert(False), 'removed item m[\'aa\'] should exist!'
 | 
			
		||||
    except KeyError, err:
 | 
			
		||||
        pass
 | 
			
		||||
    
 | 
			
		||||
    tst_range = range(10, 40) + range(50, 70)
 | 
			
		||||
    for i in tst_range:
 | 
			
		||||
        m[i] = i # will either create new, or update (m[12]), etc.s
 | 
			
		||||
 | 
			
		||||
    # test iteritems()
 | 
			
		||||
    for key, item in m.iteritems(int()):
 | 
			
		||||
        assert(key == item), 'iteritems(int()): Expected %d, but received %d' % (key, item)
 | 
			
		||||
 | 
			
		||||
    # test iterkeys()
 | 
			
		||||
    curr_index_in_range = 0
 | 
			
		||||
    for key in m.iterkeys(int()):
 | 
			
		||||
        expected = tst_range[curr_index_in_range]
 | 
			
		||||
        assert (key == expected), 'iterkeys(int): Expected %d, but received %d' % (expected, key)
 | 
			
		||||
        curr_index_in_range += 1
 | 
			
		||||
    
 | 
			
		||||
    #test itervalues()
 | 
			
		||||
    curr_index_in_range = 0
 | 
			
		||||
    for value in m.itervalues(int()):
 | 
			
		||||
        expected = tst_range[curr_index_in_range]
 | 
			
		||||
        assert (value == expected), 'itervalues(int): Expected %d, but received %d' % (expected, value)
 | 
			
		||||
        curr_index_in_range += 1
 | 
			
		||||
 | 
			
		||||
    # test items()
 | 
			
		||||
    assert (m.items(int()) == tst_range), 'm.items(int()) is not as expected.'
 | 
			
		||||
 | 
			
		||||
    # test keys()
 | 
			
		||||
    assert (m.keys(int()) == tst_range), 'm.keys(int()) is not as expected.'
 | 
			
		||||
 | 
			
		||||
    # test KeyError for non existing keys
 | 
			
		||||
    try:
 | 
			
		||||
        k =  m['323']
 | 
			
		||||
        assert(False), '__getitem__() should throw KeyError exception for non-existing key!'
 | 
			
		||||
    except KeyError, err:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    print 'All test passed OK!'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    try:
 | 
			
		||||
        test_multintermediate_key_dictionary()
 | 
			
		||||
    except Exception, err:
 | 
			
		||||
        print 'Error: ', err
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        print '\n(interrupted by user)'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user