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

switch recipe?

1 view
Skip to first unread message

Mark McEahern

unread,
Jul 12, 2002, 3:15:02 PM7/12/02
to
In response to a question earlier today, I wrote a function I called
make_color_switch:

from __future__ import generators

def make_color_switch(color1, color2):
def color_switch():
i = 0
while True:
if i % 2:
yield color2
else:
yield color1
i += 1
return color_switch

It seemed like this could be more generic:

from __future__ import generators

def make_switch(*args):
"""Return a generator that loops through args."""
if not args:
raise RuntimeError("Missing parameter: args.")
def switch():
i = n = 0
while True:
i = n % len(args)
yield args[i]
n += 1
return switch

Is switch a bad name for this? Can anyone suggest a better name? Other
improvements? What I like about this code is it demonstrates several
"advanced" features of Python, all the while retaining (imho) the simplicity
and clarity Python is known for:

generators
nested scopes
variable length argument arrays
functions as objects

Here's sample code that shows it used in the context of the original
question:

#! /usr/bin/env python

from __future__ import generators

def make_switch(*args):
"""Return a generator that loops through args."""
if not args:
raise RuntimeError("Missing parameter: args.")
def switch():
i = n = 0
while True:
i = n % len(args)
yield args[i]
n += 1
return switch

def colorize(s, *colors):
switch = make_switch(*colors)()
template = "<%(color)s><b>%(c)s</b></color>"
l = []
for c in s:
color = switch.next()
l.append(template % locals())
print ''.join(l)

colorize("testing", "black", "red", "green", "blue")

Is make_switch useful enough to post as a recipe? I didn't search to see
whether someone has already made something like this. It seems at once both
trivial and reuseful.

Cheers,

// mark

-

Sean 'Shaleh' Perry

unread,
Jul 12, 2002, 3:45:39 PM7/12/02
to

On 12-Jul-2002 Mark McEahern wrote:
> In response to a question earlier today, I wrote a function I called
> make_color_switch:
>
> from __future__ import generators
>
> def make_color_switch(color1, color2):
> def color_switch():
> i = 0
> while True:
> if i % 2:
> yield color2
>
> It seemed like this could be more generic:
>
> from __future__ import generators
>
> def make_switch(*args):
> """Return a generator that loops through args."""
> if not args:
> raise RuntimeError("Missing parameter: args.")
> def switch():
> i = n = 0
> while True:
> i = n % len(args)
> yield args[i]
> n += 1
> return switch
>

out of curiosity, what happens when n reaches the end of the int/long/whatever
that stores it?

> Is switch a bad name for this? Can anyone suggest a better name? Other
> improvements? What I like about this code is it demonstrates several
> "advanced" features of Python, all the while retaining (imho) the simplicity
> and clarity Python is known for:
>
> generators
> nested scopes
> variable length argument arrays
> functions as objects
>
> Here's sample code that shows it used in the context of the original
> question:
>

while the above code is a nice sample, the below code should not be given to
people learning the language, it uses way too many tricks. Plus one letter
variable names are a pain.

> def colorize(s, *colors):
> switch = make_switch(*colors)()
> template = "<%(color)s><b>%(c)s</b></color>"
> l = []
> for c in s:
> color = switch.next()
> l.append(template % locals())
> print ''.join(l)
>
> colorize("testing", "black", "red", "green", "blue")
>

switch is an interesting idea, definately needs a new name.


Mark McEahern

unread,
Jul 12, 2002, 4:05:42 PM7/12/02
to
> out of curiosity, what happens when n reaches the end of the
> int/long/whatever that stores it?

My guess is this will run forever--if you let it. <wink>

Perhaps it should allow an optional sentinel? The assumption is the caller
is responsible for using it wisely.

> while the above code is a nice sample, the below code should not
> be given to people learning the language, it uses way too many tricks.

I appreciate your opinion and I mean and say this with respect: One of the
joys of Python is that even supposedly advanced tricks are eminently
explorable. I would rather not patronize someone and I'd rather make the
mistake of offering them too much than too little. Besides, even if they
can't get it, they're not the only ones reading this list now or in the
future. Ultimately, I can trace this sentiment back to something very
personal: That's precisely how I'd want to be treated. Wouldn't you?

I mean, it's not like I vomitted some obscurified black hole of
context-laden punctuation (i.e., Perl) on them. <wink>

> Plus one letter variable names are a pain.

I go back and forth on this one. Presumably, you don't mean for counters
and the like (e.g., i, n). I tend to use one-letter variable names when
locals() is very small, the function is short (e.g., 5-10 lines) and the
meaning is painfully obvious. Perhaps a bad habit? E.g., s, imho, is
better than text or string for a string. I'm not trying to save typing, so
much. I strive for clarity of intent at the level of module, class,
function names. So that I don't feel so bad using one-letter variable
names. But, reasonable people disagree and I enjoy seeing alternative
styles, discussing them.

> switch is an interesting idea, definately needs a new name.

Maybe loop and make_loop? At least then the danger of an infinite one is
explicit?

Thanks for the comments.

Cheers,

// mark

-

Sean 'Shaleh' Perry

unread,
Jul 12, 2002, 4:17:06 PM7/12/02
to
>
> I appreciate your opinion and I mean and say this with respect: One of the
> joys of Python is that even supposedly advanced tricks are eminently
> explorable. I would rather not patronize someone and I'd rather make the
> mistake of offering them too much than too little. Besides, even if they
> can't get it, they're not the only ones reading this list now or in the
> future. Ultimately, I can trace this sentiment back to something very
> personal: That's precisely how I'd want to be treated. Wouldn't you?
>
> I mean, it's not like I vomitted some obscurified black hole of
> context-laden punctuation (i.e., Perl) on them. <wink>
>

the template variable ......

>> Plus one letter variable names are a pain.
>
> I go back and forth on this one. Presumably, you don't mean for counters
> and the like (e.g., i, n). I tend to use one-letter variable names when
> locals() is very small, the function is short (e.g., 5-10 lines) and the
> meaning is painfully obvious. Perhaps a bad habit? E.g., s, imho, is
> better than text or string for a string. I'm not trying to save typing, so
> much. I strive for clarity of intent at the level of module, class,
> function names. So that I don't feel so bad using one-letter variable
> names. But, reasonable people disagree and I enjoy seeing alternative
> styles, discussing them.
>

I always seem to have a semi violent reaction when I see locals().


Cliff Wells

unread,
Jul 12, 2002, 4:14:52 PM7/12/02
to
On Fri, 2002-07-12 at 13:05, Mark McEahern wrote:
> > out of curiosity, what happens when n reaches the end of the
> > int/long/whatever that stores it?
>
> My guess is this will run forever--if you let it. <wink>
>
> Perhaps it should allow an optional sentinel? The assumption is the caller
> is responsible for using it wisely.

Or you could just change it to (untested):

def make_color_switch(color1, color2):
def color_switch():
i = 0
while True:

if i:


yield color2
else:
yield color1

i = not i
return color_switch

Cliff

Chris Liechti

unread,
Jul 12, 2002, 5:21:32 PM7/12/02
to
"Mark McEahern" <ma...@mceahern.com> wrote in
news:mailman.1026501403...@python.org:

> from __future__ import generators
>
> def make_switch(*args):
> """Return a generator that loops through args."""
> if not args:
> raise RuntimeError("Missing parameter: args.")

this should be a TypeError, as the builtins raise that too

raise TypeError("switch() takes at least one argument")

> def switch():
> i = n = 0
> while True:
> i = n % len(args)
> yield args[i]
> n += 1
> return switch
>
> Is switch a bad name for this?

definetly yes ;-) as a C programmer, i expect something completly
different... (switch() {case x:})

> Can anyone suggest a better name?

how about "repeat"? 'cause that's what it does.

> Other improvements?

isn't this simpler, more obvious what it does, and still doing what you
want:

>>> from __future__ import generators
>>> def repeat(*args):
... if not args: args = [None] #optionaly to catch empty args
... while 1:
... for x in args: yield x
...
>>> zip(range(10), repeat('even', 'odd'))
[(0, 'even'), (1, 'odd'), (2, 'even'), (3, 'odd'), (4, 'even'), (5, 'odd'),
(6, 'even'), (7, 'odd'), (8, 'even'), (9, 'odd')]
>>>


chris

--
Chris <clie...@gmx.net>

Cliff Wells

unread,
Jul 12, 2002, 4:23:50 PM7/12/02
to
On Fri, 2002-07-12 at 13:05, Mark McEahern wrote:
> > out of curiosity, what happens when n reaches the end of the
> > int/long/whatever that stores it?
>
> My guess is this will run forever--if you let it. <wink>
>
> Perhaps it should allow an optional sentinel? The assumption is the caller
> is responsible for using it wisely.

Or on your "generic" version (still untested - I just reinstalled my PC
and don't have Python 2.2 yet):

from __future__ import generators

def make_switch(*args):
"""Return a generator that loops through args."""
if not args:
raise RuntimeError("Missing parameter: args.")

def switch():
i = n = 0
while True:
i = n % len(args)
yield args[i]

n = min(n + 1, len(args))
return switch

or possibly:

def make_switch(*args):
"""Return a generator that loops through args."""
if not args:
raise RuntimeError("Missing parameter: args.")

def switch():
while True:
for a in args:
yield a
return switch


Mark McEahern

unread,
Jul 12, 2002, 4:37:23 PM7/12/02
to
> or possibly:
>
> def make_switch(*args):
> """Return a generator that loops through args."""
> if not args:
> raise RuntimeError("Missing parameter: args.")
> def switch():
> while True:
> for a in args:
> yield a
> return switch

Holy cow, that's much nicer!

Thanks,

// mark
-

Mark McEahern

unread,
Jul 12, 2002, 4:35:53 PM7/12/02
to
> Or you could just change it to (untested):
>
> def make_color_switch(color1, color2):
> def color_switch():
> i = 0
> while True:
> if i:
> yield color2
> else:
> yield color1
> i = not i
> return color_switch

Nice. I guess I'm still not too worried about n maxing out. Don't ints
just spill into longs go on forever? Nevertheless, for the generic loop,
your suggestion makes me realize we can just dispense with n altogether:

def make_loop(*args):


"""Return a generator that loops through args."""
if not args:
raise RuntimeError("Missing parameter: args.")

def loop():
i = 0
while True:
i = i % len(args)
yield args[i]
i += 1
return loop

This way, the counter never gets bigger than len(args). <wink>

// m

-

Mark McEahern

unread,
Jul 12, 2002, 4:25:40 PM7/12/02
to
> the template variable ......

Good point, that certainly could have been explained better. For what it's
worth, in the original post in the other thread, I provided a list of the
features that were used so that anyone who wanted to investigate could do a
little searching to learn more. I could have spent more time compiling that
list. Thanks for the feedback.

> I always seem to have a semi violent reaction when I see locals().

Hmm, this may sound naive, but I don't understand why. Consider these
admittedly cobbled examples:

def colorize1(value, color):
template = "<%(color)s>%(value)s</%(color)s>"
return template % locals()

def colorize2(value, color):
template = "<%(color)s>%(value)s</%(color)s>"
return template % {'color': color, 'value': value}

def colorize3(value, color):
template = "<%s>%s</%s>"
return template % (color, value, color)

That's the precise order in which I'd rank them. <wink>

What could possibly be wrong with the use of locals() in colorize1()?

Cheers,

// mark

-

Alex Martelli

unread,
Jul 12, 2002, 5:51:58 PM7/12/02
to
Mark McEahern wrote:

Wouldn't you get exactly the same observable effect from
the simpler function:

def make_switch(*args):
"""Return an iterator that loops through args."""


if not args:
raise RuntimeError("Missing parameter: args.")

return iter(args)

...?


Alex

Alex Martelli

unread,
Jul 12, 2002, 5:58:45 PM7/12/02
to
Alex Martelli wrote:

...and the answer is "no", because the first function returns an
unbounded iterator, looping around the args forever, while the
second one returns a bounded iterator, looping around the
args just once. I don't think there's any substantially simpler
way to obtain the first function's effect.


Alex


Mark McEahern

unread,
Jul 12, 2002, 5:32:35 PM7/12/02
to
[Chris Liechti]

> > Is switch a bad name for this?
>
> definetly yes ;-) as a C programmer, i expect something completly
> different... (switch() {case x:})
>
> > Can anyone suggest a better name?
>
> how about "repeat"? 'cause that's what it does.

I like repeat(). That is, I like repeat! <wink> Thanks!

> isn't this simpler, more obvious what it does, and still doing what you
> want:
>
> >>> from __future__ import generators
> >>> def repeat(*args):
> ... if not args: args = [None] #optionaly to catch empty args
> ... while 1:
> ... for x in args: yield x
> ...
> >>> zip(range(10), repeat('even', 'odd'))
> [(0, 'even'), (1, 'odd'), (2, 'even'), (3, 'odd'), (4, 'even'),
> (5, 'odd'),
> (6, 'even'), (7, 'odd'), (8, 'even'), (9, 'odd')]
> >>>

Yes, that's very nice. Here's the latest, with the help of all the
suggestions I've gotten.

#! /usr/bin/env python

from __future__ import generators

def make_repeat(*args):
"""Return a generator that repeatedly loops through args."""
if not args:
raise TypeError("make_repeat() takes at least one parameter.")
def repeat():
while args:


for a in args:
yield a

return repeat()

def colorize(value, *colors):
repeat = make_repeat(*colors)
template = "<%(color)s>%(item)s</%(color)s>"
items = []
for item in value:
color = repeat.next()
formatted_item = template % locals()
items.append(formatted_item)
return ''.join(items)

s = colorize("testing", "black", "red", "green", "blue")
print s

expected = "<black>t</black>"\
"<red>e</red>"\
"<green>s</green>"\
"<blue>t</blue>"\
"<black>i</black>"\
"<red>n</red>"\
"<green>g</green>"

assert s == expected

-

Mark McEahern

unread,
Jul 12, 2002, 6:11:59 PM7/12/02
to
> Wouldn't you get exactly the same observable effect from
> the simpler function:
>
> def make_switch(*args):
> """Return an iterator that loops through args."""
> if not args:
> raise RuntimeError("Missing parameter: args.")
> return iter(args)
>
> ...?

No, sadly. The key to make_repeat() (as I've renamed it) is that it
repeatedly loops through args. The above solution would suffice if you only
wanted to iterate over the set at most once.

Of course, I'm straining to come up with real world examples where this is
helpful, but I take that as a sign of my lack of imagination. <wink>

In general, this recipe, if it's worthy to be called such, is useful when
you want to weave together a finite set of values of a given length (n) with
a different set of values of a different length (m) and for the i-th element
of n, you want it to be weaved with the len(m) modulo i-th element of m.

How's that for a fancy definition?

Of course, Alex, if there's a more elegant and simple way to do this, I have
no doubt you'll come up with it!

Cheers,

// mark

p.s. for what it's worth, here's the latest code and demo:

#! /usr/bin/env python

from __future__ import generators

def make_repeat(*args):
"""Return a generator that repeatedly loops through args."""
if not args:


raise TypeError("make_repeat() takes at least one parameter.")
def repeat():
while args:

for a in args:
yield a

Tim Peters

unread,
Jul 12, 2002, 6:17:59 PM7/12/02
to
[Cliff Wells (I think)]

> def make_switch(*args):
> """Return a generator that loops through args."""
> if not args:
> raise RuntimeError("Missing parameter: args.")
> def switch():
> while True:
> for a in args:
> yield a
> return switch

[Alex Martelli]


> Wouldn't you get exactly the same observable effect from
> the simpler function:
>
> def make_switch(*args):
> """Return an iterator that loops through args."""
> if not args:
> raise RuntimeError("Missing parameter: args.")
> return iter(args)

The top one generates an unbounded sequence, due to the "while True". The
Icon language calls this "repeated alternation", and actually has a prefix
operator for it (the vertical bar).

What I'm unclear about is why we're writing a function to return a generator
function: why not write a generator function directly? So my candidate for
simplification is:

def make_switch(*args):
"""Generate the elements in args repeatedly."""
if not args:
raise TypeError("at least one argument required")


while True:
for a in args:
yield a

I'd lose the "if not args:" block, though; if the arglist is empty, it will
simply terminate without yielding anything, and that's what I expect the
empty case to do.

Mark McEahern

unread,
Jul 12, 2002, 6:45:27 PM7/12/02
to
[Tim Peters]

> The top one generates an unbounded sequence, due to the "while True". The
> Icon language calls this "repeated alternation", and actually has a prefix
> operator for it (the vertical bar).
>
> What I'm unclear about is why we're writing a function to return
> a generator function: why not write a generator function directly?

Well dang, my original motivation was so I could pass around the generator
without specifying the args. As you point out, that's completely
unnecessary. Thanks!

> So my candidate for simplification is:
>
> def make_switch(*args):
> """Generate the elements in args repeatedly."""
> if not args:
> raise TypeError("at least one argument required")
> while True:
> for a in args:
> yield a
>
> I'd lose the "if not args:" block, though; if the arglist is
> empty, it will
> simply terminate without yielding anything, and that's what I expect the
> empty case to do.

If you leave the while True, then it won't raise StopIteration when you call
it without arguments. The simple fix is to replace that with while args.
Here's the result of my latest tinkering. For what it's worth, I think I
prefer raising the error in the generator when args is not specified rather
than waiting until next() is called the first time--on the principle that
raising the error earlier is better. Still, this version shows the delayed
raise....

#! /usr/bin/env python

from __future__ import generators
import unittest

def repeat(*args):
"""Return an unbounded iterator that repeatedly iterates over args."""
while args:


for a in args:
yield a

def weave(values, repeat, weaver):
for item in values:
yield weaver(item, repeat.next())

def color_item(item, color):


template = "<%(color)s>%(item)s</%(color)s>"

return template % locals()

class test(unittest.TestCase):

def test(self):
values = range(10)
colors = ("red", "organge", "yellow", "green", "blue", "indigo",
"violet")
repeat_colors = repeat(*colors)
expected = ['<red>0</red>',
'<organge>1</organge>',
'<yellow>2</yellow>',
'<green>3</green>',
'<blue>4</blue>',
'<indigo>5</indigo>',
'<violet>6</violet>',
'<red>7</red>',
'<organge>8</organge>',
'<yellow>9</yellow>']
generated = [x for x in weave(values, repeat_colors, color_item)]
self.assertEquals(expected, generated)

def test_empty(self):
r = repeat()
self.assertRaises(StopIteration, r.next)

if __name__ == "__main__":
unittest.main()

-

Tim Peters

unread,
Jul 12, 2002, 9:47:17 PM7/12/02
to
[Tim]

> So my candidate for simplification is:
>
> def make_switch(*args):
> """Generate the elements in args repeatedly."""
> if not args:
> raise TypeError("at least one argument required")
> while True:
> for a in args:
> yield a
>
> I'd lose the "if not args:" block, though; if the arglist is
> empty, it will simply terminate without yielding anything, and that's
> what I expect the empty case to do.

[Mark McEahern]


> If you leave the while True, then it won't raise StopIteration
> when you call it without arguments.

So true -- and even if you change it to "while 1" <wink>.

> The simple fix is to replace that with while args.

Yes, that's nice!

> Here's the result of my latest tinkering. For what it's worth, I think I
> prefer raising the error in the generator when args is not specified
> rather than waiting until next() is called the first time--on the
> principle that raising the error earlier is better.

Then that's where we differ: I don't consider an empty sequence to be "an
error" in any sense of the word. For the same reason, range(10, 10) doesn't
complain in Python, or string[i:i], etc -- doing nothing gracefully is
important because there's sometimes nothing that needs be done <wink>.

Mark McEahern

unread,
Jul 13, 2002, 2:07:27 AM7/13/02
to
[Tim Peters]

> Then that's where we differ: I don't consider an empty sequence to be "an
> error" in any sense of the word. For the same reason, range(10,
> 10) doesn't complain in Python, or string[i:i], etc -- doing nothing
> gracefully is important because there's sometimes nothing that needs
> be done <wink>.

I think I'll continue to scratch my head and profit from that observation
for quite some time. Thank you.

Profit--or better yet, lose. My misconceptions, that is. <wink>

So where I've ended up is a place I'm sure there's some fancy name for--but
I don't know it.

I have two iterators, one is finite, the other is a repeating alternator. I
want to walk through both of them at the same time, which means I'll stop
when the finite one is done. (If I need to stop sooner, I can use a
sentinel on the finite one--but I'd rather ignore this variation for now).
I want to apply some function to the items from each iterator and then yield
the result--which suggests a third iterator.

I guess I wonder whether there's a pattern here that anyone has named--or if
there's a pattern that better expresses what I can only dimly see and
express. I have trouble naming things, so again, any tips on names is
helpful.

In the following, I would express weave_items with the following pseudo
code:

for item, alternate in iterator, alternator:
yield weave_item(item, alternate)

Instead, I have to explicitly call next(), which will raise StopIteration if
I happened to pass in an alternator with no args. Because I have no way (as
far as I can tell) of asking alternator, "Do you have any more bananas?"
That is, I can't peek into it (unless I wrap it in a class or something, I
suppose):

from __future__ import generators

def repeating_alternator(*args):
"""Return a repeating alternator for args."""
while args:


for a in args:
yield a

def weave_items(iterator, alternator, weave_item):
"""Iterate over iterator, applying weave_item to each item with its
pair in alternator.
"""
for item in iterator:
yield weave_item(item, alternator.next())

def color_item(item, color):
template = "<%(color)s>%(item)s</%(color)s>"
return template % locals()

Well, that was a bunch of poorly strung together thoughts. But it's late.
And just think, I squeezed all of this out of someone's question about how
they could squelch the "extra" space emitted by print foo<comma>. Perhaps
Liebniz was right.

Cheers,

// mark

-

Mark McEahern

unread,
Jul 13, 2002, 2:20:47 AM7/13/02
to
> So where I've ended up is a place I'm sure there's some fancy
> name for--but I don't know it.

The thread on zip suggests the term "lockstep iteration." I can't use map
or zip though because I have an unbounded iterator. Of course, I could
easily bound that to the length of the finite iterator.

// m

-

Alex Martelli

unread,
Jul 13, 2002, 3:19:10 AM7/13/02
to
On Saturday 13 July 2002 00:11, Mark McEahern wrote:
...

> In general, this recipe, if it's worthy to be called such, is useful when
> you want to weave together a finite set of values of a given length (n)
> with a different set of values of a different length (m) and for the i-th
> element of n, you want it to be weaved with the len(m) modulo i-th
> element of m.

Right. So what about a direct implementation of what you just said?

def weave(setn, setm):
n = len(setn)
m = len(setm)
if not n or not m:
raise ValueError, "Weaved sets cannot be empty"
yield setn[0], setm[0]
i = 1
while i%n or i%m:
yield setn[i%n], setm[i%m]
i += 1

Your approach is more clever (and may have other uses), but this
plain and direct implementation appeals to me. So the function
that uses it might become something like:

def colorize(value, *colors):
return ''.join([ "<%s>%s</%s>" % (color, item, color)
for item, color in weave(value, colors) ])


Alex


Christian Tanzer

unread,
Jul 13, 2002, 5:48:22 AM7/13/02
to

"Mark McEahern" <mark...@mceahern.com> wrote:

> > So where I've ended up is a place I'm sure there's some fancy
> > name for--but I don't know it.
>
> The thread on zip suggests the term "lockstep iteration." I can't use
> map or zip though because I have an unbounded iterator.

Not so. As long as one of the iterators is finite, zip will work:

>>> from __future__ import generators
>>> def repeat(*args):
... """Return a repeating alternator for args."""
... while args:
... for a in args:
... yield a
...
>>> for i, j in zip(range(5), repeat("a","b","c")):
... print i, j
...
0 a
1 b
2 c
3 a
4 b

--
Christian Tanzer tan...@swing.co.at
Glasauergasse 32 Tel: +43 1 876 62 36
A-1130 Vienna, Austria Fax: +43 1 877 66 92

Fredrik Lundh

unread,
Jul 13, 2002, 8:37:47 AM7/13/02
to
Mark McEahern

> The thread on zip suggests the term "lockstep iteration." I can't use map
> or zip though because I have an unbounded iterator. Of course, I could
> easily bound that to the length of the finite iterator.

zip stops as soon as the first sequence runs out of items:

class over_and_over_again:
def __init__(self, seq):
self.seq = seq
self.len = len(seq)
def __getitem__(self, index):
return self.seq[index % self.len]

print zip(range(12), over_and_over_again("spam"))

prints

[(0, 's'), (1, 'p'), (2, 'a'), (3, 'm'), (4, 's'), (5, 'p'),
(6, 'a'), (7, 'm'), (8, 's'), (9, 'p'), (10, 'a'), (11, 'm')]

</F>


Mark McEahern

unread,
Jul 13, 2002, 10:08:55 AM7/13/02
to
[Christian Tanzer]

> Not so. As long as one of the iterators is finite, zip will work:
>
> >>> from __future__ import generators
> >>> def repeat(*args):
> ... """Return a repeating alternator for args."""
> ... while args:
> ... for a in args:
> ... yield a
> ...
> >>> for i, j in zip(range(5), repeat("a","b","c")):
> ... print i, j
> ...
> 0 a
> 1 b
> 2 c
> 3 a
> 4 b

I just saw Office Space for the first time last night. One of the
characters in the movie develops a prototype for a Jumping To Conclusions
mat. That's certainly what I did about zip, assuming that it had similar
behavior to map when it came to unbounded iterators. As you point out,
that's not true.

Because I want to do something to the zipped items, it doesn't seem like zip
solves all my problems, though. map would, if it had the same behavior as
zip--namely that it stopped once one of the iterators ran out of items.
Below is a version that shows the use of zip. Here are the key lines:

weaved = zip(iterator, alternator)
generated = [color_item(x, y) for x, y in weaved]

Perhaps what I'm still looking for is a way to
weave-two-iterators-with-a-function all at once. One solution would be to
modify or wrap repeating_alternator() so that it was finite--then I could
just use map.

Thanks!

// mark

#! /usr/bin/env python

from __future__ import generators
import unittest

def repeating_alternator(*args):


"""Return a repeating alternator for args."""

while args:
for a in args:
yield a

def weave_items(iterator, alternator, weave_item):
"""Iterate over iterator, applying weave_item to each item with its
pair in alternator.
"""
for item in iterator:
yield weave_item(item, alternator.next())

def color_item(item, color):
template = "<%(color)s>%(item)s</%(color)s>"
return template % locals()

class test(unittest.TestCase):

def test(self):
iterator = range(10)


colors = ("red", "organge", "yellow", "green", "blue", "indigo",
"violet")

alternator = repeating_alternator(*colors)


expected = ['<red>0</red>',
'<organge>1</organge>',
'<yellow>2</yellow>',
'<green>3</green>',
'<blue>4</blue>',
'<indigo>5</indigo>',
'<violet>6</violet>',
'<red>7</red>',
'<organge>8</organge>',
'<yellow>9</yellow>']

weaved = zip(iterator, alternator)
generated = [color_item(x, y) for x, y in weaved]
self.assertEquals(expected, generated)

def test_empty(self):
r = repeating_alternator()

Emile van Sebille

unread,
Jul 13, 2002, 10:56:50 AM7/13/02
to
Mark McEahern

> Because I want to do something to the zipped items, it doesn't seem
like zip
> solves all my problems, though. map would, if it had the same
behavior as
> zip--namely that it stopped once one of the iterators ran out of
items.
> Below is a version that shows the use of zip. Here are the key lines:
>
> weaved = zip(iterator, alternator)
> generated = [color_item(x, y) for x, y in weaved]
>
> Perhaps what I'm still looking for is a way to
> weave-two-iterators-with-a-function all at once. One solution would
be to
> modify or wrap repeating_alternator() so that it was finite--then I
could
> just use map.

doesn't this do it?

generated = [color_item(x, alternator) for x in iterator]

(BTW, I nominate cycler as a name for alternator ;-)

--

Emile van Sebille
em...@fenx.com

---------

Terry Reedy

unread,
Jul 13, 2002, 12:10:55 PM7/13/02
to

"Mark McEahern" <mark...@mceahern.com> wrote in message > Perhaps

what I'm still looking for is a way to
> weave-two-iterators-with-a-function all at once. One solution would
be to
> modify or wrap repeating_alternator() so that it was finite--then I
could
> just use map.

*IF* you know the 'length' of iterator, or the iterable it is based
on, as in the examples used in preious articles in this thread, this
is easy and clean:

def firstn(it, n):
while n:
yield it.next()
n -= 1

n = len(iterator) # or whatever
map(color_item, iterator, firstn(alternator, n))

Terry J. Reedy

Alex Martelli

unread,
Jul 13, 2002, 5:16:17 PM7/13/02
to
Terry Reedy wrote:
...

> is easy and clean:
>
> def firstn(it, n):
> while n:
> yield it.next()
> n -= 1

Marginally easier and cleaner:

def take(n, it):
for j in xrange(n):
yield it.next()


Alex

Mark McEahern

unread,
Jul 15, 2002, 3:31:22 PM7/15/02
to
[Emile van Sebille]

> doesn't this do it?
>
> generated = [color_item(x, alternator) for x in iterator]
>
> (BTW, I nominate cycler as a name for alternator ;-)

I like cycler, too. I'm going to post a summary of solutions shortly. The
above works if you say alternator.next().

// m


-

Mark McEahern

unread,
Jul 15, 2002, 3:31:23 PM7/15/02
to
I want to thank everyone who posted to this thread. I've gotten many
excellent suggestions. The following demo presents all the distinct
variations I culled from the thread. I like Emile's suggestion of the name
cycler, but I didn't use it in this demo--I think partly because for better
or worse alternator has lodged itself in my brain.

Cheers,

// mark

#! /usr/bin/env python

from __future__ import generators
import unittest

def weave(setn, setm):
"""Return a generator that iterates over setn and setm until setn is
exhausted. If setn is larger than setm, cycle over setm.
"""
m = len(setm)
if not setn or not setm:


raise ValueError, "Weaved sets cannot be empty"

for i in range(len(setn)):
yield setn[i], setm[i%m]

def finite_alternator(length, *args):
"""Return a finite repeating alternator for args."""
c = 0
while args and c < length:


for a in args:
yield a

c += 1
if c >= length:
break

def take(n, it):
"""Return n elements from the unbounded iterator."""


for j in xrange(n):
yield it.next()

def repeating_alternator(*args):


"""Return a repeating alternator for args."""
while args:
for a in args:
yield a

def weave_items(iterator, alternator, weave_item):
"""Iterate over iterator, applying weave_item to each item with its
pair in alternator.
"""
for item in iterator:
yield weave_item(item, alternator.next())

def color_item(item, color):
template = "<%(color)s>%(item)s</%(color)s>"
return template % locals()

class test(unittest.TestCase):

def setUp(self):
self.colors = ("red", "orange", "yellow", "green", "blue", "indigo",
"violet")
self.expected = ['<red>0</red>',
'<orange>1</orange>',


'<yellow>2</yellow>',
'<green>3</green>',
'<blue>4</blue>',
'<indigo>5</indigo>',
'<violet>6</violet>',
'<red>7</red>',

'<orange>8</orange>',
'<yellow>9</yellow>']
self.length = 10

def test_weave(self):
colors = self.colors
length = self.length
expected = self.expected

generated = [color_item(x, y) for x, y in weave(range(length),
colors)]
self.assertEquals(expected, generated)

def test_zip(self):
colors = self.colors
length = self.length
expected = self.expected

iterator = range(length)
alternator = repeating_alternator(*colors)


weaved = zip(iterator, alternator)
generated = [color_item(x, y) for x, y in weaved]

self.assertEquals(expected, generated)

def test_list_comprehension(self):
colors = self.colors
length = self.length
expected = self.expected

iterator = range(length)
alternator = repeating_alternator(*colors)
generated = [color_item(x, alternator.next()) for x in iterator]
self.assertEquals(expected, generated)

def test_map(self):
colors = self.colors
length = self.length
expected = self.expected

iterator = range(length)
alternator = finite_alternator(length, *colors)
generated = map(color_item, iterator, alternator)
self.assertEquals(expected, generated)

def test_map2(self):
colors = self.colors
length = self.length
expected = self.expected

iterator = range(length)
alternator = repeating_alternator(*colors)
generated = map(color_item, iterator, take(length, alternator))
self.assertEquals(expected, generated)

def test_weave_items(self):
colors = self.colors
length = self.length
expected = self.expected

iterator = range(length)
alternator = repeating_alternator(*colors)
generated = [x for x in weave_items(iterator, alternator,
color_item)]

Mark McEahern

unread,
Jul 15, 2002, 3:31:21 PM7/15/02
to
[Alex Martelli]

> Right. So what about a direct implementation of what you just said?
>
> def weave(setn, setm):
> n = len(setn)
> m = len(setm)
> if not n or not m:

> raise ValueError, "Weaved sets cannot be empty"
> yield setn[0], setm[0]
> i = 1
> while i%n or i%m:
> yield setn[i%n], setm[i%m]
> i += 1

I like this. The 'or' in the while statment means this will loop until
lcd(n, m)--easily fixed. I modified this slightly:

def weave(setn, setm):
"""Return a generator that iterates over setn and setm until setn is
exhausted. If setn is larger than setm, cycle over setm.
"""
m = len(setm)
if not setn or not setm:
raise ValueError, "Weaved sets cannot be empty"
for i in range(len(setn)):
yield setn[i], setm[i%m]

This means you have to specify the alternator/cycler second--it doesn't
treat the two sets as interchangeable. Slightly less general than the
direction your solution points.

// m

-

Alex Martelli

unread,
Jul 16, 2002, 5:29:37 AM7/16/02
to
Mark McEahern wrote:

> [Alex Martelli]
>> Right. So what about a direct implementation of what you just said?
>>
>> def weave(setn, setm):
>> n = len(setn)
>> m = len(setm)
>> if not n or not m:
>> raise ValueError, "Weaved sets cannot be empty"
>> yield setn[0], setm[0]
>> i = 1
>> while i%n or i%m:
>> yield setn[i%n], setm[i%m]
>> i += 1
>
> I like this. The 'or' in the while statment means this will loop until
> lcd(n, m)--easily fixed. I modified this slightly:

I'm not sure what an lcd is in this context, but I did mean to
loop least-common-multiple(m, n) times, treating the two sets
as equivalent. So I guess I had misunderstood the specs!


> def weave(setn, setm):
> """Return a generator that iterates over setn and setm until setn is
> exhausted. If setn is larger than setm, cycle over setm.
> """
> m = len(setm)
> if not setn or not setm:
> raise ValueError, "Weaved sets cannot be empty"
> for i in range(len(setn)):
> yield setn[i], setm[i%m]
>
> This means you have to specify the alternator/cycler second--it doesn't
> treat the two sets as interchangeable. Slightly less general than the
> direction your solution points.

Yep. Nicer in Python 2.3 of course:

def weave(setn, setm):
"""Return a generator that iterates over setn and setm until setn is
exhausted. If setn is larger than setm, cycle over setm."""

if not setn or not setm:
raise ValueError, "Weaved sets cannot be empty"

m = len(setm)
for index, item in enumerate(setn):
yield item, setm[index%m]

Here, actually, an empty setn SHOULD be quite acceptable -- as no
cycling is done on it, that should be OK, and only an empty setm
should be a problem. So, I'd prefer:

def weave(setn, setm):
"""Return an iterator that iterates over iterator setn and
sequence setm until setn is exhausted. If setn is larger than

setm, cycle over setm."""
m = len(setm)

if not m:
raise ValueError, "Set to be cycled on cannot be empty"
for index, item in enumerate(setn):
yield item, setm[index%m]


Alex

Mark McEahern

unread,
Jul 16, 2002, 8:46:19 AM7/16/02
to
[Alex Martelli]

> I'm not sure what an lcd is in this context, but I did mean to
> loop least-common-multiple(m, n) times, treating the two sets
> as equivalent. So I guess I had misunderstood the specs!

I seem to have a penchant for maligning concepts. Thanks for the correction
on denominator --> multiple. Would it make sense to say lcd (least common
denominator) is a conceptual subset of lcm (least common multiple)? If so,
that explains my confusion.

I like your 2.3 version, especially making it handle empty setn:

> def weave(setn, setm):
> """Return an iterator that iterates over iterator setn and
> sequence setm until setn is exhausted. If setn is larger than
> setm, cycle over setm."""
> m = len(setm)
> if not m:
> raise ValueError, "Set to be cycled on cannot be empty"
> for index, item in enumerate(setn):
> yield item, setm[index%m]

I posted a summary of all the code presented in this thread here:

http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/139210

Thanks!

// mark

-

Alex Martelli

unread,
Jul 16, 2002, 9:43:12 AM7/16/02
to
Mark McEahern wrote:

> [Alex Martelli]
>> I'm not sure what an lcd is in this context, but I did mean to
>> loop least-common-multiple(m, n) times, treating the two sets
>> as equivalent. So I guess I had misunderstood the specs!
>
> I seem to have a penchant for maligning concepts. Thanks for the
> correction
> on denominator --> multiple. Would it make sense to say lcd (least common
> denominator) is a conceptual subset of lcm (least common multiple)? If

Hmmm, the lcd of minimal-terms fractions A and B is the lcm of A's
denominator and B's denominator, right? Guess it IS a "conceptual
subclass" in that you have to add the concepts of denominator and
minimal-terms:-).


Alex

0 new messages