On Sep 17, 5:56 pm, Lee Harr <miss...@hotmail.comwrote:
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 choiceweights[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)