Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

decorator and API

20 views
Skip to first unread message

Lee Harr

unread,
Sep 17, 2008, 5:56:29 PM9/17/08
to pytho...@python.org

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.

_________________________________________________________________
Explore the seven wonders of the world
http://search.msn.com/results.aspx?q=7+wonders+world&mkt=en-US&form=QBRE

Aaron "Castironpi" Brady

unread,
Sep 17, 2008, 7:09:41 PM9/17/08
to

What about a function, 'reweight', which wraps the original, and sets
a weight on the wrapper?

def reweight(value):
def reset_weight(method):
#@wraps(method) #optional
def new_method( *ar, **kw ):
return method( *ar, **kw )
new_method.weight = value
return new_method
return reset_weight

Call like this:

class B(A):
action_1= reweight( 50 )( A.action_1 )

You could pass them both in to reweight with two parameters:

class B(A):
action_1= reweight( 50, A.action_1 )

It's about the same. Variable-signature functions have limits.

Otherwise, you can keep dictionaries by name, and checking membership
in them in superclasses that have them by hand. Then you'd need a
consistent name for the dictionary. That option looks like this
(unproduced):

class A:
__weights__= {}
@weight( __weights__, 10 ) ...
@weight( __weights__, 20 ) ...

class B( A ):
__weights__= {} #new instance so you don't change the original
@weight( __weights__, 50 ) ...

B.__weight__ could be an object that knows what it's overriding
(unproduced):

class A:
weights= WeightOb() #just a dictionary, mostly
@weights( 10 ) ...
@weights( 20 ) ...

class B( A ):
weights= WeightOb( A.weights ) #new, refs "super-member"
@weights( 50 ) ...

If either of the last two options look promising, I think I can
produce the WeightOb class. It has a '__call__' method.

Aaron "Castironpi" Brady

unread,
Sep 17, 2008, 7:45:40 PM9/17/08
to
On Sep 17, 6:09 pm, "Aaron \"Castironpi\" Brady"

<castiro...@gmail.com> wrote:
> On Sep 17, 4:56 pm, Lee Harr <miss...@hotmail.com> wrote:
>
>
>
> > I have a class with certain methods from which I want to select
> > one at random, with weighting.
(snip)

>
> > 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.
>
> class A:
>    weights= WeightOb() #just a dictionary, mostly
>    @weights( 10 ) ...
>    @weights( 20 ) ...
>
> class B( A ):
>    weights= WeightOb( A.weights ) #new, refs "super-member"
>    @weights( 50 ) ...

Lee,

Probably overkill. Here's a solution like above.

class WeightOb( object ):
def __init__( self, *supers ):
self.weights= {}
self.supers= supers
def set( self, weight ):
def __callset__( fun ):
self.weights[ fun.func_name ]= weight
return fun
return __callset__
def reset( self, weight, fun ):
self.weights[ fun.func_name ]= weight
return fun
#search parent 'weight' objects
#return 'child-most' weight of 'name'
def get_weight( self, name ):
if name in self.weights:
return self.weights[ name ]
else:
for x in self.supers:
try:
return x.get_weight( name )
except KeyError: #not found
pass
raise KeyError
#returns a dictionary mapping bound instances to weights
#(hence the second parameter)
def contents( self, inst ):
d= {}
for x in reversed( self.supers ):
d.update( x.contents( inst ) )
d.update( dict( [
( getattr( inst, k ), v ) for k, v in self.weights.iteritems( ) ] ) )
return d


class A( object ):
weights= WeightOb( )
@weights.set( 10 )
def action_1( self ):
print 'action_1'
@weights.set( 20 )
def action_2( self ):
print 'action_2'
#WeightOb.contents needs to know which instance to bind
#functions to. Get weights from an instance that has them.
def getweights( self ):
return self.weights.contents( self )

class B( A ):
weights= WeightOb( A.weights )

action_2= weights.reset( 50, A.action_2 )

a= A()
b= B()
print a.weights.get_weight( 'action_1' )
print a.weights.get_weight( 'action_2' )
print b.weights.get_weight( 'action_1' )
print b.weights.get_weight( 'action_2' )
print a.getweights( )
print b.getweights( )


/Output:

10
20
10
50
{<bound method A.action_2 of <__main__.A object at 0x00A04070>>: 20,
<bound meth
od A.action_1 of <__main__.A object at 0x00A04070>>: 10}
{<bound method B.action_2 of <__main__.B object at 0x00A04090>>: 50,
<bound meth
od B.action_1 of <__main__.B object at 0x00A04090>>: 10}

Steven D'Aprano

unread,
Sep 17, 2008, 11:35:52 PM9/17/08
to
On Thu, 18 Sep 2008 02:26:29 +0430, Lee Harr 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 ....

[snip]


You are coupling the weights, the actions, and the object which chooses
an action all in the one object. I find that harder to wrap my brain
around than a more loosely coupled system. Make the chooser independent
of the things being chosen:


def choose_with_weighting(actions, weights=None):
if weights is None:
weights = [1]*len(actions) # equal weights
# Taken virtually unchanged from your code.
# I hope it does what you want it to do!
assert len(weights) == len(actions)


total = sum(weights)
choice = random.randrange(total)
while choice > weights[0]:
choice -= weights[0]
weights.pop(0)
actions.pop(0)
return actions[0]


Loosely couple the actions from their weightings, so you can change them
independently. Here's a decorator that populates a dictionary with
weights and actions:

def weight(value, storage):
def set_weight(method):
storage[method.__name__] = value
return method
return set_weight


Here's how to use it:

class A(object):
weights = {}
def __init__(self):
self.weights = self.__class__.weights.copy()
@weight(10, weights)
def action_1(self):
print "A.action_1"
@weight(20, weights)
def action_2(self):
print "A.action_2"


The class is now populated with a set of default weights, which is then
copied to the instance. If you want to over-ride a particular weight, you
don't need to make a subclass, you just change the instance:

obj = A()
obj.weights["action_1"] = 30

method = choose_with_weighting(obj.weights.keys(), obj.weights.values())
getattr(obj, method)() # call the method

Hope this helps,

--
Steven

George Sakkis

unread,
Sep 18, 2008, 2:20:27 AM9/18/08
to


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)

Peter Otten

unread,
Sep 18, 2008, 5:32:47 AM9/18/08
to
Steven D'Aprano wrote:

I agree with you that the simple explicit approach is better.
Now, to answer the question the OP didn't ask:

> def choose_with_weighting(actions, weights=None):
>     if weights is None:
>         weights = [1]*len(actions)  # equal weights
>     # Taken virtually unchanged from your code.
>     # I hope it does what you want it to do!

It probably doesn't.

>     assert len(weights) == len(actions)
>     total = sum(weights)
>     choice = random.randrange(total)
>     while choice > weights[0]:
>         choice -= weights[0]
>         weights.pop(0)
>         actions.pop(0)
>     return actions[0]

Assume two actions with equal weights [1, 1]. total becomes 2, and choice is
either 0 or 1, but never > weights[0].

While this can be fixed by changing the while condition to

while choice >= weights[0]: #...

I prefer an approach that doesn't destroy the actions and weights lists,
something like

import bisect

def choose_with_weighting(actions, weights=None, acc_weights=None):
if acc_weights is None:
if weights is None:
return random.choice(actions)
else:
sigma = 0
acc_weights = []
for w in weights:
sigma += w
acc_weights.append(sigma)
return actions[bisect.bisect(acc_weights,
random.randrange(acc_weights[-1]))]

especially if you prepare the acc_weights list once outside the function.

Peter

Gerard flanagan

unread,
Sep 18, 2008, 6:31:45 AM9/18/08
to pytho...@python.org
Lee Harr 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]
>
>

Here is another approach:

8<-------------------------------------------------------------------

import random
from bisect import bisect

#by George Sakkis
def take_random_action(obj, actions, weights):


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.random())](obj)

class randomiser(object):

_cache = []

@classmethod
def alert(cls, func):
assert hasattr(func, 'weight')
cls._cache.append(func)

@classmethod
def register(cls, name, obj):
actions = {}
weights = []
for klass in obj.__class__.__mro__:
for val in klass.__dict__.itervalues():
if hasattr(val, '__name__'):
key = val.__name__
if key in actions:
continue
elif val in cls._cache:
actions[key] = val
weights.append(val.weight)
actions = actions.values()
#setattr(cls, name, classmethod(lambda cls:
random.choice(actions)(obj)))
setattr(cls, name, classmethod(lambda cls:
take_random_action(obj, actions, weights)))

def randomised(weight):
def wrapper(func):
func.weight = weight
randomiser.alert(func)
return func
return wrapper

class A(object):

@randomised(20)
def foo(self):
print 'foo'

@randomised(10)
def bar(self):
print 'bar'

class B(A):

@randomised(50)
def foo(self):
print 'foo'

8<-------------------------------------------------------------------

randomiser.register('a', A())
randomiser.register('b', B())
print 'A'
randomiser.a()
randomiser.a()
randomiser.a()
randomiser.a()
randomiser.a()
randomiser.a()
print 'B'
randomiser.b()
randomiser.b()
randomiser.b()
randomiser.b()
randomiser.b()
randomiser.b()


0 new messages