added multi_key_dictionary

This commit is contained in:
Lukasz Forynski
2013-05-31 00:26:31 +01:00
parent bf4d16ed79
commit 761de74472

279
multi_key_dictionary.py Normal file
View 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)'