decorator and API
George Sakkis
george.sakkis at gmail.com
Thu Sep 18 02:20:27 EDT 2008
On Sep 17, 5:56 pm, Lee Harr <miss... at hotmail.com> 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)
More information about the Python-list
mailing list