Dynamic Dictionary Creation

Bengt Richter bokr at oz.net
Fri Dec 6 22:25:57 EST 2002


On Fri, 06 Dec 2002 11:37:07 -0700, Bob van der Poel <bvdpoel at kootenay.com> wrote:

>
>I'm writing a program which needs to convert note lengths specified in
>somewhat standard musical notation to midi ticks. I have a function
>which does this with
>a dictionary lookup. An abreviated version is shown here:
>
>def getNoteLen(x):
>   global TicksQ
>   ntb = { '1': TicksQ *
>4,                                                               
>      '2': TicksQ * 2,                                   
>      '4': TicksQ,
>      '8': TicksQ
>}                                                                  
>                                                                                                
>   return ntb[str(x)]  
>
>What I'm concerned about is the initialization of the table. Am I
>correct in assuming that each time I access the function the values in
>the table will be recalculated?
Yes, as written above.

>
>I suppose I could make the table global and avoid this?
>
Yes, but if the table is only used in the function, that's not nice.
There are several alternatives you could use.

>I could avoid runtime math by replacing TicksQ with a value, but I've
>always been taught not to use literal magic numbers, and python doesn't
>have a DEFINE statement.

Just binding the name to a value is normal, but if you reeeaaalllly want to,
you can generate code with the value built in as a constant, even though
you've bound it in one place to a symbol. In this case we don't need to,
because the name is dereferenced when the ntb dict literal is evaluated.

BTW, to see what code is in your actual function, you can use the dis module.
import dis; dis.dis(foo) will list the byte code of function foo. Even if
you don't understand everything you see, you will get an idea of the amount
of stuff done at run time, and you can compare various versions of your code.

You might also want to consider making your function a method of a class,
and making your table a class variable. That way it's generated when the class
is defined, not when the method runs.

An old and simple way to give a function a predefined value to work with
is to add an extra parameter to the calling parameters, and give it a default
value. That way, it's calculated when the definition executes, not when the
function is called. Access to the value is local and fast, so it's good that
way, but it is a bit hokey that it's actually a parameter that can be overridden
if you pass an actual one in that position. Using this methodology, your function
would be:

 >>> TicksQ = 5 # just to give it a value
 >>> def getNoteLen(
 ...     x,
 ...     ntb= {'1': TicksQ *4,'2': TicksQ * 2,'4': TicksQ, '8': TicksQ}
 ... ):
 ...     return ntb[str(x)]
 ...
 >>> getNoteLen(2)
 10
 >>> import dis
 >>> dis.dis(getNoteLen) 
           0 SET_LINENO               1

           3 SET_LINENO               5
           6 LOAD_FAST                1 (ntb)
           9 LOAD_GLOBAL              1 (str)
          12 LOAD_FAST                0 (x)
          15 CALL_FUNCTION            1
          18 BINARY_SUBSCR
          19 RETURN_VALUE
          20 LOAD_CONST               0 (None)
          23 RETURN_VALUE

or some variation of the above. BTW, integers make perfectly fine dictionary
keys, so if the x argument is always an integer, you ought to write it as

 >>> def getNoteLen(x, ntb={1:TicksQ*4, 2:TicksQ*2, 4:TicksQ, 8:TicksQ}):
 ...     return ntb[x]
 ...
 >>> getNoteLen(2)
 10
 >>> dis.dis(getNoteLen)
           0 SET_LINENO               1

           3 SET_LINENO               2
           6 LOAD_FAST                1 (ntb)
           9 LOAD_FAST                0 (x)
          12 BINARY_SUBSCR
          13 RETURN_VALUE
          14 LOAD_CONST               0 (None)
          17 RETURN_VALUE

We note that str(x) is eliminated.

BTW, TicksQ wouldn't need a global declaration unless you were going
to rebind it (i.e., "assign" to it). But we're not using a global here.

BTW2, the ntb default argument contains values for the TicksQ expressions,
and no longer any reference to TicksQ:

 >>> import inspect
 >>> inspect.getargspec(getNoteLen)
 (['x', 'ntb'], None, None, ({8: 5, 1: 20, 2: 10, 4: 5},))
         ^^^-----------------^^^^^^^^^^^^^^^^^^^^^^^^^^

A class based alternative might be

 >>> class MidiStuff:
 ...     ntb = {1:TicksQ*4, 2:TicksQ*2, 4:TicksQ, 8:TicksQ}
 ...     def getNoteLen(x):
 ...         return MidiStuff.ntb[x]
 ...     getNoteLen = staticmethod(getNoteLen)
 ...
 >>> MidiStuff.getNoteLen(2)
 10
 >>> dis.dis(MidiStuff.getNoteLen)
           0 SET_LINENO               3

           3 SET_LINENO               4
           6 LOAD_GLOBAL              0 (MidiStuff)
           9 LOAD_ATTR                1 (ntb)
          12 LOAD_FAST                0 (x)
          15 BINARY_SUBSCR
          16 RETURN_VALUE
          17 LOAD_CONST               0 (None)
          20 RETURN_VALUE

(BTW here ntb also has evaluated values):

 >>> MidiStuff.ntb
 {8: 5, 1: 20, 2: 10, 4: 5}

This is using a class something like a mini module, but it's not
so nice that it is compiled to refer to itself dynamically via
its global name. So there is an alternative, which binds the class
(not an instance!) to the 'self' (or first) parameter of the method:

 >>> class MidiStuff:
 ...     ntb = {1:TicksQ*4, 2:TicksQ*2, 4:TicksQ, 8:TicksQ}
 ...     def getNoteLen(self, x):
 ...         return self.ntb[x]
 ...     getNoteLen = classmethod(getNoteLen)
 ...
 >>> MidiStuff.getNoteLen(2)
 10
 >>> dis.dis(MidiStuff.getNoteLen)
           0 SET_LINENO               3

           3 SET_LINENO               4
           6 LOAD_FAST                0 (self)
           9 LOAD_ATTR                1 (ntb)
          12 LOAD_FAST                1 (x)
          15 BINARY_SUBSCR
          16 RETURN_VALUE
          17 LOAD_CONST               0 (None)
          20 RETURN_VALUE

This is virtually the same code, but we don't have to look up
our global class name to find ourself. It's still an extra
step compared to having ntb as a default parameter though, where we
just got ntb with a LOAD_FAST instead of LOAD_FAST (self) followed
by LOAD_ATTR (ntb) here.

How else to provide the function with the ntb dictionary without using
a gobal?  A closure can hold stuff. It's what you get if you define a
function inside a another function's local space, and have the latter
return the defined function. The local stuff used by the inside defined
function would go away if it were not captured somehow, and a closure
does that:

 >>> def ff():
 ...     ntb = {1:TicksQ*4, 2:TicksQ*2, 4:TicksQ, 8:TicksQ}
 ...     def getNoteLen(x):
 ...         return ntb[x]
 ...     return getNoteLen
 ...
 >>> getNoteLen = ff()
 >>> getNoteLen(2)
 10
 >>> dis.dis(getNoteLen)
           0 SET_LINENO               3

           3 SET_LINENO               4
           6 LOAD_DEREF               0 (ntb)
           9 LOAD_FAST                0 (x)
          12 BINARY_SUBSCR
          13 RETURN_VALUE
          14 LOAD_CONST               0 (None)
          17 RETURN_VALUE

This gets ntb with the single operation LOAD_DEREF, which hopefully should be fast
and comparable to getting it via default parameter, which notice there isn't any:

 >>> inspect.getargspec(getNoteLen)
 (['x'], None, None, None)

So you have a number of options. Which is best is probably the one that
fits best into your overall design and will be easy to understand when
you come back to it later.

Regards,
Bengt Richter



More information about the Python-list mailing list