On Sep 17, 5:56 pm, Lee Harr <[EMAIL PROTECTED]> wrote: > I have a class with certain methods from which I want to select > one at random, with weighting. > > The way I have done it is this .... > > import random > > def weight(value): > def set_weight(method): > method.weight = value > return method > return set_weight > > class A(object): > def actions(self): > 'return a list of possible actions' > > return [getattr(self, method) > for method in dir(self) > if method.startswith('action_')] > > def action(self): > 'Select a possible action using weighted choice' > > actions = self.actions() > weights = [method.weight for method in actions] > total = sum(weights) > > choice = random.randrange(total) > > while choice> weights[0]: > choice -= weights[0] > weights.pop(0) > actions.pop(0) > > return actions[0] > > @weight(10) > def action_1(self): > print "A.action_1" > > @weight(20) > def action_2(self): > print "A.action_2" > > a = A() > a.action()() > > The problem I have now is that if I subclass A and want to > change the weighting of one of the methods, I am not sure > how to do that. > > One idea I had was to override the method using the new > weight in the decorator, and then call the original method: > > class B(A): > @weight(50) > def action_1(self): > A.action_1(self) > > That works, but it feels messy. > > Another idea was to store the weightings as a dictionary > on each instance, but I could not see how to update that > from a decorator. > > I like the idea of having the weights in a dictionary, so I > am looking for a better API, or a way to re-weight the > methods using a decorator. > > Any suggestions appreciated.
Below is a lightweight solution that uses a descriptor. Also the random action function has been rewritten more efficiently (using bisect). George #======== usage =========================== class A(object): # actions don't have to follow a naming convention @weighted_action(weight=4) def foo(self): print "A.foo" @weighted_action() # default weight=1 def bar(self): print "A.bar" class B(A): # explicit copy of each action with new weight foo = A.foo.copy(weight=2) bar = A.bar.copy(weight=4) @weighted_action(weight=3) def baz(self): print "B.baz" # equivalent to B, but update all weights at once in one statement class B2(A): @weighted_action(weight=3) def baz(self): print "B2.baz" update_weights(B2, foo=2, bar=4) if __name__ == '__main__': for obj in A,B,B2: print obj for action in iter_weighted_actions(obj): print ' ', action a = A() for i in xrange(10): take_random_action(a) print b = B() for i in xrange(12): take_random_action(b) #====== implementation ======================= class _WeightedActionDescriptor(object): def __init__(self, func, weight): self._func = func self.weight = weight def __get__(self, obj, objtype): return self def __call__(self, *args, **kwds): return self._func(*args, **kwds) def copy(self, weight): return self.__class__(self._func, weight) def __str__(self): return 'WeightedAction(%s, weight=%s)' % (self._func, self.weight) def weighted_action(weight=1): return lambda func: _WeightedActionDescriptor(func,weight) def update_weights(obj, **name2weight): for name,weight in name2weight.iteritems(): action = getattr(obj,name) assert isinstance(action,_WeightedActionDescriptor) setattr(obj, name, action.copy(weight)) def iter_weighted_actions(obj): return (attr for attr in (getattr(obj, name) for name in dir(obj)) if isinstance(attr, _WeightedActionDescriptor)) def take_random_action(obj): from random import random from bisect import bisect actions = list(iter_weighted_actions(obj)) weights = [action.weight for action in actions] total = float(sum(weights)) cum_norm_weights = [0.0]*len(weights) for i in xrange(len(weights)): cum_norm_weights[i] = cum_norm_weights[i-1] + weights[i]/total return actions[bisect(cum_norm_weights, random())](obj) -- http://mail.python.org/mailman/listinfo/python-list