11

There is already a multi key dict in python and also a multivalued dict. I needed a python dictionary which is both:

example:

# probabilistically fetch any one of baloon, toy or car
d['red','blue','green']== "baloon" or "car" or "toy"  

Probability of d['red']==d['green'] is high and Probability of d['red']!=d['red'] is low but possible

the single output value should be probabilistically determined (fuzzy) based on a rule from keys eg:in above case rule could be if keys have both "red" and "blue" then return "baloon" 80% of time if only blue then return "toy" 15% of time else "car" 5% of time.

The setitem method should be designed such that following is possible:

d["red", "blue"] =[
    ("baloon",haseither('red','green'),0.8),
    ("toy",.....)
    ,....
]

Above assigns multiple values to the dictionary with a predicate function and corresponding probability. And instead of the assignment list above even a dictionary as assignment would be preferable:

d["red", "blue"] ={ 
    "baloon": haseither('red','green',0.8),
    "toy": hasonly("blue",0.15),
    "car": default(0.05)
}

In the above baloon will be returned 80% of time if "red" or green is present , return toy 15% of time if blue present and return car 5% of time without any condition.

Are there any existing data structures which already satisfy the above requirements in python? if no then how can multikeydict code be modified to meet the above requirements in python?

if using dictionary then there can be a configuration file or use of appropriate nested decorators which configures the above probabilistic predicate logics without having to hard code if \else statements .

Note: Above is a useful automata for a rule based auto responder application hence do let me know if any similar rule based framework is available in python even if it does not use the dictionary structure?

tmthydvnprt
  • 10,398
  • 8
  • 52
  • 72
stackit
  • 3,036
  • 9
  • 34
  • 62
  • From what I understand, the multi-key part is like a "synonym" key: different ways to refer to the same thing, like 1000, k, kilo in the multi-key-dict readme. My question is: when you say multi-value, Are you saying synonym values? or many different values? – chapelo May 01 '15 at 19:56
  • What do you mean by `d['red']==d['green']` ? Is that because you've looked up a multi-key including both of those before? I understood your question to mean that d['red'] != d['red'] necessarily... – Andy Hayden May 02 '15 at 02:30
  • @chapelo ideally it should have provision for both synonym values as well as different values , to be configured by a "setting" in the dictionary. – stackit May 02 '15 at 06:50
  • @AndyHayden Yes completely you are right and yes d['red'] != d['red'] because of probabilistic fetch – stackit May 02 '15 at 06:51
  • @stackit so to clarify, you know what all the possible inputs and what the percentages ought to be (you expect to be able to right this as if blocks with random returns)? Or are these numbers calculated from somewhere? – Andy Hayden May 02 '15 at 06:59
  • @AndyHayden Yes percentages supplied by the user – stackit May 02 '15 at 07:01
  • @stackit at the moment / in your example, the percentages don't add up. It's incomplete - there are more cases to deal with than that. – Andy Hayden May 02 '15 at 07:02
  • 1
    WoJ has the right idea, there's no need for this to be dict-like - it's a function. Making it dict-like is obscurification. :( – Andy Hayden May 02 '15 at 07:03
  • @AndyHayden making it dict like makes it generic and some benefits of OOP – stackit May 02 '15 at 07:50
  • 1
    What about setting an item? Say you have `d['red','blue','green']= "baloon" or "car" or "toy"` with the probabilities set for those things. What happens with `d['red']='ball'`? – dawg May 02 '15 at 15:38
  • 'useful for a rule based auto responder'. can you show what kinda rule you want to make based on real world example? like 'temperature' is 'low', 'medium' or 'high'. – Nizam Mohamed May 07 '15 at 17:01
  • @NizamMohamed say eg email auto responder based on key words k1,k2, etc: if (k2 and k1): return mesg1 10% of time and mesg2 90% times ;if only k2 return mesg3 etc.. all this in a dictionary interface like d[k1,k2..... with a nondeterministic output – stackit May 07 '15 at 21:04
  • @dawg this is explained in comments of Andy's answer. – stackit May 07 '15 at 21:07
  • '% of time' means the dict has to remember how many times it has output a particular value. say, it has output 100 times in total and it should have output 90 times mesg2 and 10 times mesg1. It seems illogical. reformulate the idea and bring in some other varaibles, like 'this key' and 'that value' than msg1. – Nizam Mohamed May 07 '15 at 22:26
  • `multi_key_dict` doesn't support more than one key fetch. If 'key' than either msg1 or msg2 is possible, that's random. – Nizam Mohamed May 07 '15 at 22:38
  • @NizamMohamed Not needed to remember as it is probabilistic outcome andy has given an example how to do that. And it should support multikey fetch as that is what the question is about. – stackit May 08 '15 at 04:45
  • @NizamMohamed I have added more explanation – stackit May 08 '15 at 05:07
  • `if 'red' in colors and 'blue' in colors: if random.random() < 0.8` this is multi condition. If the first is `True`, second may not be `True` and same goes to other conditions. – Nizam Mohamed May 08 '15 at 07:11

4 Answers4

4

the single output value should be probabilistically determined (fuzzy) based on a rule from keys eg:in above case rule could be if keys have both "red" and "blue" then return "baloon" 80% of time if only blue then return "toy" 15% of time else "car" 5% of time.

Bare in mind your case analysis is not complete, and it's ambiguous, but you can do the following "in spirit" (fleshing out the desired results):

import random

def randomly_return(*colors):
    colors = set(*colors)
    if 'red' in colors and 'blue' in colors:
        if random.random() < 0.8:  # 80 % of the time
            return "baloon"

    if 'blue' in colors and len(colors) == 1:  # only blue in colors
        if random.random() < 0.15:
            return "toy"
        else:
            if random.random() < 0.05:
                return "car"

# other cases to consider

I would keep this as a function, because it is a function! But if you insist to make it dict-like, then python let's you do this by overriding __getitem__ (IMO it's not pythonic).

class RandomlyReturn(object):
    def __getitem__(self, *colors):
        return randomly_return(*colors)

>>> r = RandomlyReturn()
>>> r["red", "blue"]  # 80% of the time it'll return "baloon"
"baloon"

From your clarification, OP wants to pass and generate:

randreturn((haseither(red,blue),baloon:0.8),((hasonly(blue),toy:0.15)),(default(‌​),car:0.05)))

you want to generate a function as follows:

funcs = {"haseither": lambda needles, haystack: any(n in haystack for n in needles),
         "hasonly": lambda needles, haystack: len(needles) == 1 and needles[1] in haystack}

def make_random_return(crits, default):
    def random_return(*colors):
        colors = set(*colors)
        for c in crits:
            if funcs[c["func"]](c["args"], colors) and random.random() > c["with_prob"]:
                return c["return_value"]
        return default
    return random_return

where the crit and default in this case would be:

crit = [{"func": "haseither", "args": ("red", "blue"), "return_value": "baloon", "with_prob": 0.8}, ...]
default = "car"  # ??
my_random_return = make_random_return(crits, default)

As I say, your probabilities are ambiguous/don't add up, so you're most likely going to need to tweak this...

You can extend the class definition by passing crit and default upon instantiation:

class RandomlyReturn(object):
    def __init__(self, crit, default):
        self.randomly_return = make_random_return(crit, default)
    def __getitem__(self, *colors):
        return self.randomly_return(*colors)

>>> r = RandomlyReturn(crit, default)
>>> r["red", "blue"]  # 80% of the time it'll return "baloon"
"baloon"
Andy Hayden
  • 359,921
  • 101
  • 625
  • 535
  • Thanks ,but it needs to not use logics in the function , but independently specified either as a decorator or seperate configuration eg:a decorator randreturn(baloon:0.8,toy:0.15, car:0.05) – stackit May 02 '15 at 07:53
  • the getitem will have the above decorator – stackit May 02 '15 at 07:55
  • randreturn((has(red,blue),baloon:0.8),(...)) – stackit May 02 '15 at 07:58
  • above is a nested decorator to be used for getitem function in the dictionary defination – stackit May 02 '15 at 08:00
  • randreturn((haseither(red,blue),baloon:0.8),((hasonly(blue),toy:0.15)),(default(),car:0.05))) – stackit May 02 '15 at 08:03
  • So the function to return is just iterate through the criteria (with suitably defined haseither and hasonly), make sense? – Andy Hayden May 02 '15 at 08:14
  • @stackit This isn't a decorater, as a decorator takes a function and returns a new function. You want a function which you pass criteria and return a function (which you can then generate a class or whatever). – Andy Hayden May 02 '15 at 08:34
  • it is a nested decorator – stackit May 02 '15 at 08:52
  • eg: randreturn((haseither(red,blue),baloon:0.8), .... is a nested decorator which recieves two functions one is the getitem and other the predicate function(haseither etc) and returns a single function which is used to generate the dict like class – stackit May 02 '15 at 08:59
  • Also , how would you use make_random_return function as it does not have color argument, a subfunction has it. – stackit May 03 '15 at 01:27
  • @stackit make_random_return makes your random_return function (labelled by the criteria). If you wanted it dict_like you have to pass this to the `__init__` of the class, if you use the function then it's returned from make_random_return. Make sense? – Andy Hayden May 03 '15 at 03:48
  • Thanks, but also all other dict primitives like setitem etc should also be defined for a complete solution – stackit May 03 '15 at 08:28
  • "also all other dict primitives like setitem etc should also be defined", you can fill these out yourself. e.g. setitem could append to an internal crits dict. – Andy Hayden May 03 '15 at 17:44
  • can you post a complete modified multikey dict code which incorporates these features? As I want to see how this would work using multikeydict – stackit May 06 '15 at 03:21
  • @stackit I feel like I've done several edits on this, will try and make time to do one final edit tomorrow. To do the final part, you just have to make `__setitem__` assign to the internal crit dictionary... everything you need is already laid out. – Andy Hayden May 06 '15 at 04:27
  • Thanks for your contribution, I was wondering how to handle other dict primitives like update, convert to values, getting keys etc.. to make a complete implementation. They should be simple as for now. eg: get all keys and possible values in flat order. – stackit May 06 '15 at 04:42
  • @andy-hayden `set` takes an iterable. – Nizam Mohamed May 06 '15 at 20:44
  • @andy-hayden I said you unpack `colors = set(*colors)`, an error. – Nizam Mohamed May 06 '15 at 21:10
  • @NizamMohamed it's not an error, because colors is an iterable! – Andy Hayden May 06 '15 at 21:20
  • 1
    @NizamMohamed you can always modify above code or write a new answer(preferable) – stackit May 07 '15 at 14:16
  • @stackit just remove the *. – Andy Hayden May 07 '15 at 17:49
  • I have added more explanation – stackit May 08 '15 at 05:07
  • @AndyHayden do let me know if any more info is required for complete implementation – stackit May 08 '15 at 13:03
  • @stackit on holiday at the moment, will put something together when back. Maybe as another answer. Thanks! – Andy Hayden May 08 '15 at 15:16
  • @stackit or not, ok then! – Andy Hayden May 08 '15 at 22:17
4

Simulated MultiKey Dictionary

multi_key_dict did not allow __getitem__() with multiple keys at onces...

(e.g. d["red", "green"])

A multi key can be simulated with tuple or set keys. If order does not matter, set seems the best (actually the hashable frozen set, so that ["red", "blue"] is the same a ["blue", "red"].

Simulated MultiVal Dictionary

Multi values are inherent by using certain datatypes, it can be any storage element that may be conveniently indexed. A standard dict should provide that.

Non-determinism

Using a probability distribution defined by the rules and assumptions1, non-deterministic selection is performed using this recipe from the python docs.

MultiKeyMultiValNonDeterministicDict Class

What a name.   \o/-nice!

This class takes multiple keys that define a probabilistic rule set of multiple values. During item creation (__setitem__()) all value probabilities are precomputed for all combinations of keys1. During item access (__getitem__()) the precomputed probability distribution is selected and the result is evaluated based on a random weighted selection.

Definition

import random
import operator
import bisect
import itertools

# or use itertools.accumulate in python 3
def accumulate(iterable, func=operator.add):
    'Return running totals'
    # accumulate([1,2,3,4,5]) --> 1 3 6 10 15
    # accumulate([1,2,3,4,5], operator.mul) --> 1 2 6 24 120
    it = iter(iterable)
    try:
        total = next(it)
    except StopIteration:
        return
    yield total
    for element in it:
        total = func(total, element)
        yield total

class MultiKeyMultiValNonDeterministicDict(dict):

    def key_combinations(self, keys):
        """get all combinations of keys"""
        return [frozenset(subset) for L in range(0, len(keys)+1) for subset in itertools.combinations(keys, L)]

    def multi_val_rule_prob(self, rules, rule):
        """
        assign probabilities for each value, 
        spreading undefined result probabilities
        uniformly over the leftover results not defined by rule.
        """
        all_results = set([result for result_probs in rules.values() for result in result_probs])
        prob = rules[rule]
        leftover_prob = 1.0 - sum([x for x in prob.values()])
        leftover_results = len(all_results) - len(prob)
        for result in all_results:
            if result not in prob:
                # spread undefined prob uniformly over leftover results
                prob[result] = leftover_prob/leftover_results
        return prob

    def multi_key_rule_prob(self, key, val):
        """
        assign probability distributions for every combination of keys,
        using the default for combinations not defined in rule set
        """ 
        combo_probs = {}
        for combo in self.key_combinations(key):
            if combo in val:
                result_probs = self.multi_val_rule_prob(val, combo).items()
            else:
                result_probs = self.multi_val_rule_prob(val, frozenset([])).items()
            combo_probs[combo] = result_probs
        return combo_probs

    def weighted_random_choice(self, weighted_choices):
        """make choice from weighted distribution"""
        choices, weights = zip(*weighted_choices)
        cumdist = list(accumulate(weights))
        return choices[bisect.bisect(cumdist, random.random() * cumdist[-1])]

    def __setitem__(self, key, val):
        """
        set item in dictionary, 
        assigns values to keys with precomputed probability distributions
        """

        precompute_val_probs = self.multi_key_rule_prob(key, val)        
        # use to show ALL precomputed probabilities for key's rule set
        # print precompute_val_probs        

        dict.__setitem__(self, frozenset(key), precompute_val_probs)

    def __getitem__(self, key):
        """
        get item from dictionary, 
        randomly select value based on rule probability
        """
        key = frozenset([key]) if isinstance(key, str) else frozenset(key)             
        val = None
        weighted_val = None        
        if key in self.keys():
            val = dict.__getitem__(self, key)
            weighted_val = val[key]
        else:
            for k in self.keys():
                if key.issubset(k):
                    val = dict.__getitem__(self, k)
                    weighted_val = val[key]

        # used to show probabality for key
        # print weighted_val

        if weighted_val:
            prob_results = self.weighted_random_choice(weighted_val)
        else:
            prob_results = None
        return prob_results

Usage

d = MultiKeyMultiValNonDeterministicDict()

d["red","blue","green"] = {
    # {rule_set} : {result: probability}
    frozenset(["red", "green"]): {"ballon": 0.8},
    frozenset(["blue"]): {"toy": 0.15},
    frozenset([]): {"car": 0.05}
}

Testing

Check the probabilities

N = 10000
red_green_test = {'car':0.0, 'toy':0.0, 'ballon':0.0}
red_blue_test = {'car':0.0, 'toy':0.0, 'ballon':0.0}
blue_test = {'car':0.0, 'toy':0.0, 'ballon':0.0}
red_blue_green_test = {'car':0.0, 'toy':0.0, 'ballon':0.0}
default_test = {'car':0.0, 'toy':0.0, 'ballon':0.0}

for _ in xrange(N):
    red_green_test[d["red","green"]] += 1.0
    red_blue_test[d["red","blue"]] += 1.0
    blue_test[d["blue"]] += 1.0
    default_test[d["green"]] += 1.0
    red_blue_green_test[d["red","blue","green"]] += 1.0

print 'red,green test      =', ' '.join('{0}: {1:05.2f}%'.format(key, 100.0*val/N) for key, val in red_green_test.items())
print 'red,blue test       =', ' '.join('{0}: {1:05.2f}%'.format(key, 100.0*val/N) for key, val in red_blue_test.items())
print 'blue test           =', ' '.join('{0}: {1:05.2f}%'.format(key, 100.0*val/N) for key, val in blue_test.items())
print 'default test        =', ' '.join('{0}: {1:05.2f}%'.format(key, 100.0*val/N) for key, val in default_test.items())
print 'red,blue,green test =', ' '.join('{0}: {1:05.2f}%'.format(key, 100.0*val/N) for key, val in red_blue_green_test.items())

red,green test      = car: 09.89% toy: 10.06% ballon: 80.05%
red,blue test       = car: 05.30% toy: 47.71% ballon: 46.99%
blue test           = car: 41.69% toy: 15.02% ballon: 43.29%
default test        = car: 05.03% toy: 47.16% ballon: 47.81%
red,blue,green test = car: 04.85% toy: 49.20% ballon: 45.95%

Probabilities match rules!


Footnotes

  1. Distribution Assumption

    Since the rule set is not fully defined, assumptions are made about the probability distributions, most of this is done in multi_val_rule_prob(). Basically any undefined probability will be spread uniformly over the remaining values. This is done for all combinations of keys, and creates a generalized key interface for the random weighted selection.

    Given the example rule set

    d["red","blue","green"] = {
        # {rule_set} : {result: probability}
        frozenset(["red", "green"]): {"ballon": 0.8},
        frozenset(["blue"]): {"toy": 0.15},
        frozenset([]): {"car": 0.05}
    }
    

    this will create the following distributions

    'red'           = [('car', 0.050), ('toy', 0.475), ('ballon', 0.475)]
    'green'         = [('car', 0.050), ('toy', 0.475), ('ballon', 0.475)]
    'blue'          = [('car', 0.425), ('toy', 0.150), ('ballon', 0.425)]
    'blue,red'      = [('car', 0.050), ('toy', 0.475), ('ballon', 0.475)]
    'green,red'     = [('car', 0.098), ('toy', 0.098), ('ballon', 0.800)]
    'blue,green'    = [('car', 0.050), ('toy', 0.475), ('ballon', 0.475)]
    'blue,green,red'= [('car', 0.050), ('toy', 0.475), ('ballon', 0.475)]
     default        = [('car', 0.050), ('toy', 0.475), ('ballon', 0.475)]
    

    If this is incorrect, please advise.

tmthydvnprt
  • 10,398
  • 8
  • 52
  • 72
  • that looks to be a great implementation, thanks, I will soon check it and share doubts if any. I had many praises for this but comment system does not allow it – stackit May 08 '15 at 18:05
  • in the default case , you used "green" key what makes you assume that "green" is default key? – stackit May 08 '15 at 18:24
  • 1 also the dictionary keys should be frozenset as the order of keys dont matter and it needs to be hashable. I had used that construct for illustrative example. – stackit May 08 '15 at 18:31
  • 2 what if another dict assignment comes later like d["blue"]== {frozenset(["blue"]): {"doll": 0.55} ...} how will this be handled? – stackit May 08 '15 at 18:36
  • and will this program be tractable if the keys are say 10 keys ? 10!=3628800 – stackit May 08 '15 at 18:52
  • You didn't say it had to be efficient, lol! :) If there are that many keys then the precomputed part should be evaluated on the fly. – tmthydvnprt May 08 '15 at 19:13
  • Your right about the reassignment issue, but that partially seems to be the same as any reassignment in any programming situation. That will need to be up to the use case to have robust rule sets that are defined all at once. I'll see it there is a better way to handle it though. – tmthydvnprt May 08 '15 at 19:16
  • I used `green` key as default case to show that it wasn't individually set by the rules... If that is not how it is supposed to work, then a slightly different rule logic will need to be written... – tmthydvnprt May 08 '15 at 19:22
  • on doing the above reassignment it throws an error line 35, in multi_val_rule_prob prob = rules[rule] KeyError: frozenset([]) – stackit May 08 '15 at 19:29
  • Ok I think there is another assumption that the default has to be defined in the original rules. I didn't add any error handling, type checking, etc. since it is an example. – tmthydvnprt May 08 '15 at 19:33
  • what type checking is needed in this? – stackit May 08 '15 at 19:56
  • Not sure, just depending on your use case, say you tried to input a list or tuple instead on dictionary for the inner values of the rule set... – tmthydvnprt May 08 '15 at 21:47
1

If it is possible to change the data structure, it would be simpler to have a function returning the data you need. This will be completely flexible and could accommodate any kind of data, should you need to change them later.

import random

def myfunc(*args):
    if 'red' in args:
        return 'blue'
    elif 'green' in args or 'violet' in args:
        return 'violet'
    else:
        r = random.random()
        if 0 < r < 0.2:
            return 'blue'
        else:
            return 'green'

print(myfunc('green', 'blue'))
print(myfunc('yellow'))

output (the second line obviously changes):

violet
blue
WoJ
  • 27,165
  • 48
  • 180
  • 345
  • Yes it is possible , you can give an example – stackit May 01 '15 at 05:00
  • Thanks , but how to incorporate this in the multidict structure – stackit May 01 '15 at 12:45
  • It is not possible, these data containers are expected to hold static (deterministic) values. Is there a specific reason why you do not want to go for a function-based approach? – WoJ May 01 '15 at 18:44
  • 1
    classes can also have this function, how would you incorporate above in multikey dict – stackit May 01 '15 at 19:13
  • I am not sure i understand: the functions in classes (methods) are "normal" functions, just in a specific context. Again: a dict holds deterministic values, so your "fuzzy" answer cannot be implemented directly (you could pre-compute the values (once, or on a timely basis) so that whatever is stored in the dict is still deterministic, but with a value which changes from time to time). But I have a hard time understanding why you want to use a dict for that, as opposed to a function. – WoJ May 01 '15 at 19:17
  • want to use a dict as it is convenient , easy to use and makes it more maintainable. – stackit May 01 '15 at 19:32
  • I can understand that you want to use this static data structure but it will not hold non-deterministic values. – WoJ May 01 '15 at 19:41
  • so can't the dictionary itself be made non deterministic? – stackit May 02 '15 at 06:57
  • @stackit why do you want the dictionary to be non-deterministic rather than generate a dict-like structure which is... like the above code does. It seems like this is semantics rather than substance. The above code does what you want but in "the wrong way"? – Andy Hayden May 02 '15 at 17:16
  • violates OOP principles? – stackit May 07 '15 at 14:11
  • I have added more explanation – stackit May 08 '15 at 05:07
1

The OP wants as follows,

d["red", "blue"] ={ 
    "baloon": haseither('red','green',0.8),
    "toy": hasonly("blue",0.15),
    "car": default(0.05)
}  

but this is data with embeded logic. It's very tedious to define a function for every value. What I suggest is to seprate the data and logic.

Python has a data type for this, that's class. A callable instance of a class can be assigned to the dict and let the dict pass the keys and call the object to return the result.

I've inherited and extended multiple_key_dict to support multi-key fetch and to pass keys to the object and call the object which has been stored in the dict.

I assume data is recalculated per rule. This is Rule class, it has list of rules. A rule is a Python expressions and it has access to len function and keys list. So one can write a rule like len(keys) == 1 and 'blue' in keys.

class Rule(object):

    def __init__(self, rule, data):
        self.rule = rule
        self.data = data

This is Data class which has both set of data and rules.

class Data(object):
    def __init__(self, rules):
        self.rules= rules

    def make_choice(self, data):
        data = tuple(self.make_list_of_values(data))
        return random.choice(data)

    def make_list_of_values(self, data):
        for val, weight in data:
            percent = int(weight * 100)
            for v in [val] * percent:
                yield v

    def __call__(self, keys):
        for rule in self.rules:
            if eval(rule.rule,dict(keys=keys)):
                return self.make_choice(rule.data)

This is RuleDict, but non-callables can not be fetched.

class RuleDict(multi_key_dict):
    def __init__(self, *args, **kwargs):
        multi_key_dict.__init__(self, *args, **kwargs)

    def __getitem__(self, keys):
        if isinstance(keys, str):
            keys = (keys, )
        keys_set = frozenset(keys)
        for key in self.keys():
            key = frozenset(key)
            if keys_set <= key:
                return multi_key_dict.__getitem__(self,keys[0])(keys)
        raise KeyError(keys)

usage example,

d = RuleDict()
rule1 = Rule('"red" in keys and "green" in keys',(('baloon',0.8), ('car',0.05), ('toy',0.15)))
rule2 = Rule('len(keys) ==1 and "blue" in keys',(('baloon',0.25), ('car',0.35), ('toy',0.15)))
data = Data((rule1, rule2))
d['red','blue','green'] = data

print(d['red','green'])  

d['red','green'] calls the object, with keys, that was assigned and return the result.

Another approach is, to make the dict callable. This one seems a sound approach, because data and logic are separate. By this, you pass the keys and the logic, a callable, to the dict and return the result. f.e.,

def f(keys, data):
    pass # do the logic and return data

d['red','blue','green'] = ('baloon', 'car', 'toy')

Now call the dict

d(('red','blue'),f)

This is callable dict. If no callable is given, just returns the whole data.

class callable_mkd(multi_key_dict):
    def __init__(self, *args, **kwargs):
        multi_key_dict.__init__(self, *args, **kwargs)

    def __call__(self, keys, process=None):
        keys_set = frozenset(keys)
        for key in self.keys():
            key = frozenset(key)
            if keys_set <= key:
                if process:
                    return process(keys, self[keys[0]])
                return self[keys[0]]
        raise KeyError(keys)
Nizam Mohamed
  • 8,751
  • 24
  • 32