from numbers import Real class ProbDict(dict): """A dictionary serving as a probability measure.""" def __init__(self, items = None): """Create a dictionary from iters. If items can't be fed to a dictionary, it will be interpreted as a collection of keys, and each value will default to value 1/n. Otherwise, the values are normalized to sum to one. Raises ValueError if some values are not numbers or are negative. Arguments: - `items`: argument with which to make dictionary """ if items is None: return dict.__init__(self) try: # can fail if items is not iterable or not full of size 2 items: dict.__init__(self, items) except TypeError: try: # Let's assume items is a finite iterable full of keys vals = [1/len(items)] * len(items) except TypeError: # Apparently items has no length -- let's take it as the only # key and put all the probability on it. dict.__init__(self, (items, 1)) else: # if items has a length, it can be iterated through with zip dict.__init__(self, zip(items, vals)) else: # we've successfully made dic from key, value pairs in items, now let's # normalize the dictionary, and check the values for v in self.values(): if not isinstance(v, Real): raise TypeError("Values must be nonnegative real numbers so I " + "can properly normalize them. " + str(v) + " is not.") elif v < 0: raise ValueError("Values must be nonnegative, unlike " + str(v)) tot = sum(self.values()) for k, v in self.items(): self[k] = v/tot def __setitem__(self, key, value): # Overridden to make sure dict is normalized. if not isinstance(value, Real) or value < 0 or value > 1: raise ValueError("Value must be a number between 0 and 1, unlike " + str(i)) try: r = (self[key] - value)/(1 - self[key]) # r is the fraction of the remaining probability mass that # we're going to give up (take). for k in filter(key.__ne__, self): dict.__setitem__(self, k, self[k] * (1 + r)) value = value if len(self) != 1 else 1 if value: dict.__setitem__(self, key, value) else: # This is the purging stage! dict.__delitem__(self, key) except ZeroDivisionError: # self[key] = 1, so key has all the probability mass. We'll leave it # as is, since there's no sensible way of reducing it. pass def __delitem__(self, key): # Deleting frees up probability mass! self[key] = 0 # Note that __setitem__ handles the deletion for us. def __missing__(self, key): # Accessing an inexistent key gives 0 rather than error, but # does not create key, val pair (unlike defaultdict) return 0