James Tauber

journeyman of some

blog > 2008 > 05 > 02 >

Factoring Out Common Args To Zipped Generators

I'm playing around with some additive synthesis in Python.

I've implemented an oscillator as a generator that takes a number of parameters. It is then possible to mix multiple oscillators using zip (or better, itertools.izip) over them and doing a (weighted) sum.

However, I wanted to be able to factor out common arguments to the oscillators so I didn't have to specify the frequency of each one individually.

I knew functools.partial would be part of the solution but it took me a while to work out how to combine its use with generators and itertools.izip.

Here is a simplified progression of what I came up with.

Phase 1

Rather than use oscillators, let's just imagine with have a generator that works a lot like xrange:

def gen1(start, stop, step): n = start while n <= stop: yield n n += step

then we can combine multiple generators and, say, sum the corresponding elements like this:

for x in zip(gen1(10, 20, 2), gen1(10, 25, 3)): print sum(x),

Phase 2

Let's abstract this into a function that takes generators as arguments (and uses itertools.izip)

def mixer1(*generators): return (sum(x) for x in izip(*generators))

for x in mixer1(gen1(10, 20, 2), gen1(10, 25, 3)): print x,

mixer1 is similar to my mixer (although without weighting)

Phase 3

But now say we wanted to factor out the common start parameter. First we need a partial version of function gen1:

gen2 = lambda **kwargs: partial(gen1, **kwargs)

This allows one to say

partial_gen = gen2(stop=20, step=2)

and then later call

partial_gen(start=10)

to get the generator.

But what we now need is a new version of the mixer that takes the extra keyword args and passes them in to each partial function to turn them back into generators:

def mixer2(*generators, **kwargs): return mixer1(*[gen(**kwargs) for gen in generators])

and now we can say:

for x in mixer2(gen2(stop=20, step=2), gen2(stop=25, step=3), start=10): print x,

Phase 4

Here's the final version:

gen2 = lambda **kwargs: partial(gen1, **kwargs)

def mixer3(*generators, **kwargs): return (sum(x) for x in izip(*[gen(**kwargs) for gen in generators]))

The real thing is a little more involved because of the weighted summing, etc but the hard parts are shown.

Categories:
prev « python » next

Comments (1)

J on May 11, 2008:

That's really neat.

Maybe, a Phase 5 (for lazy programmers) to be put into some utility module:

def mixgens(*generators, **common_args):
return izip(*[gen(**common_args) for gen in generators])

def mixer3(*generators, **common_args):
return (sum(x) for x in mixgens(*generators, **common_args))

Created: May 2, 2008
Last Modified: May 3, 2008
Author: James Tauber