[Tutor] class attribute to initiate more classes

Dave Angel davea at ieee.org
Sat Oct 31 16:47:31 CET 2009


(You top-posted, which confuses the sequence of message text.  So I 
clipped it off and posted my message at the bottom, which is the 
convention on this newsgroup)

Vincent Davis wrote:
> DaveA posted
> import random, functools
>
> class Person:
>     def __init__(self, size):
>         self.size = size
>
>         def __str__(self):
>             return "Person of size %s" % self.size
>
> class MakePeople:
>     def __init__(self, random_func):
>         self.random_func = random_func
>
>     def make_them(self, count):
>         return [Person(self.random_func()) for i in xrange(count)]
>
> people_maker = MakePeople(functools.partial(random.gauss, 100, 2))
> persons = people_maker.make_them(100)
> for person in persons:
>     print person.size
>
> I changed the last line, from
> print person
> print person.size
>
> So this does what I want, but I am not sure why.
> I read the entry about functools.partial but it was not very clear to me.
> If I
> people_maker = MakePeople(random.gauss(100, 2))
> then I only get 1 random #.
> and if I
> MakePeople('random.gauss(100, 2)')
> then I just a fix string
> So DaveA uses
>
> functools.partial(random.gauss, 100, 2)
>
> not obvious to me from that it should not be
>
> functools.partial(random.gauss(100, 2))
>
> and I guess the other key is
>
> Person(self.random_func())
>
> Also now this
> people_maker = MakePeople(123)
>
> does not work, which is not terrible.
>
> Anyone have some more to add, I would not have confidence in applying this
> to new situations and it seems.
>
> Also I thank DaveA improving my Python conventions. I am really bad about
> that. Is there a cheat sheet for Python conventions.
>
> Like class (Capitals), def (two_words), I guess I should make my own.
>
> Thanks
> Vincent Davis
> 720-301-3003
>
> <snip>
>
>   
For the official Python style guide, see 
http://www.python.org/dev/peps/pep-0008/

There are a couple of things going on in my code sample, and I'll try to 
elaborate on them.  I'm not claiming it's the 'right' answer, just that 
it satisfies what I think were the most important goals you had.  But if 
you want to save the list in the MakePeople() instance, you'd add an 
additional parameter to its constructor, and combine the second method 
into __init__().  But as someone else points out, at that point, it's 
then hardly worth making a class out of it.

First the tough part.  In your original code, your caller was doing:
 

listofp = makepeople(random.guass(100, 2))

But that passes only a single value into the makepeople constructor.  If you're always going to use gaussian, then you can just move the function call into the make_them() loop.  But if you want to do the same thing, but with a different distribution, you need to pass a function object instead.  Learning about function objects is very useful.  You should really play with them in a simpler situation, to get an idea how they work.

At its simplest, a function object is just the function name, without parentheses.  You can store it (and pass it as a parameter to another function) just like any other object.  And then when you actually want to call it, you can use the new name with parentheses, kind of like an alias to the original function name.

import math

def indirection(funcobject, argument):
    return funcobject(math.pi/180 * argument)

print indirection(math.sin, 30)
print indirection(math.cos, 30)


Now what happens here?  The indirection() function calls an entirely 
different function the two times it's called, one time it calls sin, and 
the other time it calls cos.  As long as all the arguments to the 
function are going to be supplied here, there's no confusion.  Try the 
same thing with any other set of functions, given that all of them take 
the same number and types of arguments.

This opens the door to all sorts of things, such as a mini-calculator 
(following is mostly pseudo-code):

funcname =  getnamefrom user()
angleinradians = getnumberfromuser()

converter_dict = { "sin": math.sin,  "cos":math.cos }
print  converter_dict[funcname](angleinradians)

So the values of the dictionary are actual function objects, ready to be 
called.

What happens if some of the function parameters are known to the caller, 
and not to the callee?  I'll use the random.gauss example again.  If we 
want the caller to be able to specify them, we could do something like 
the following:

def print_indirection(funcobject, arg1, arg2, count):
    for i in xrange(count):
        print funcobject(arg1, arg2)

and call it:
    print_indirection(random.gauss, 100, 2, 44)

to get 44 values with a gaussian distributon.  But now suppose we wanted 
to be able to use the same function with random.getrandbits() ?  That 
function only takes a single parameter, so the print funcobject() would 
blow up.

functools.partial lets us  bind a function object with some or all of 
its arguments already attached.  So in our case, the caller (who knows 
what the arguments look like) gets to lay them out, without being stuck 
with any particular ones.

So, we rewrite print_indirection something like this:

def print_indirection(funcobject, count):
    for i in xrange(count):
        print funcobject()

and call it like:
    print_indirection(functools.partial(random.gauss, 100, 2), 44)
    print_indirection(functools.partial(random.getrandbits, 17),    44)

The key here is that
     functools.partial(random.gauss, 100, 2)
is a function object that takes no parameters, because the 2 parameters 
of the underlying function have already been bound.

Now this can be generalized a bit, but I hope you understand it better 
than before.  You have to play with it.  Make lists or dicts of your own 
functions, with or without binding parameters.  The key is that such a 
list should have uniform signatures, which in the examples I've shown 
means none of the function objects take any (more) arguments.

DaveA



More information about the Tutor mailing list