decorator and API

George Sakkis george.sakkis at gmail.com
Thu Sep 18 08:20:27 CEST 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