views:

234

answers:

6

I've got a loop that wants to execute to exhaustion or until some user specified limit is reached. I've got a construct that looks bad yet I can't seem to find a more elegant way to express it; is there one?

def ello_bruce(limit=None):
    for i in xrange(10**5):
        if predicate(i):
            if not limit is None:
                limit -= 1
                if limit <= 0:
                   break

def predicate(i):
    # lengthy computation
    return True

Holy nesting! There has to be a better way. For purposes of a working example, xrange is used where I normally have an iterator of finite but unknown length (and predicate sometimes returns False).

+2  A: 

I'd take a good look at the itertools library. Using that, I think you'd have something like...

# From the itertools examples
def tabulate(function, start=0):
    return imap(function, count(start))
def take(n, iterable):
    return list(islice(iterable, n))

# Then something like:
def ello_bruce(limit=None):
  take(filter(tabulate(predicate)), limit)
Boojum
+1 indeed, that module has some subtle powers (or powerful subtleties).
msw
I think the parameters to `take` are reversed, i.e. you have `def take(n, iterable)`, but you call it like `take(iterable, n)`.
bcat
+10  A: 

Maybe something like this would be a little better:

from itertools import ifilter, islice

def ello_bruce(limit=None):
    for i in islice(ifilter(predicate, xrange(10**5)), limit):
        # do whatever you want with i here
bcat
+1 Nice solution!
Xavier Ho
perfect, thank you.
msw
This crushes way too much into one line; the original code is clearer. It'd help a lot to split the nesting apart; `iter = ifilter(predicate, xrange(10**5))` and then `for i in islice(iter, limit)`.
Glenn Maynard
@Glenn: I know it's a matter of style, but I respectfully disagree. I think adding a variable as you proposed hurts more than it helps because it makes it harder to see exactly what's going on. Without the variable, I think it's clear that the code takes an iterator, filters it, and slices the first `limit` elements off of the filtered iterator. After the variable is added, I have to read an additional, intermediate step before I understand what the code does.
bcat
@bcat: It took me about twenty seconds to parse your code. The original code was instantly obvious, without taking any real thought at all. That makes the original code much, much better than yours as it's currently written (even despite the unnecessary nesting in the original).
Glenn Maynard
@Glenn: My experience was just the opposite. It took me some 20 or 30 seconds to figure out the OP's code (largely due, I think, to the awkward structure of the `limit`-related code). I find my code much easier to mentally process. Just start at the inside of the expression and work your way out, and you've got it.
bcat
+1  A: 

I'd start with

if limit is None: return

since nothing can ever happen to limit when it starts as None (if there are no desirable side effects in the iteration and in the computation of predicate -- if there are, then, in this case you can just do for i in xrange(10**5): predicate(i)).

If limit is not None, then you just want to perform max(limit, 1) computations of predicate that are true, so an itertools.islice of an itertools.ifilter would do:

import itertools as it

def ello_bruce(limit=None):
    if limit is None:
        for i in xrange(10**5): predicate(i)
    else:
        for _ in it.islice(
          it.ifilter(predicate, xrange(10**5),
          max(limit, 1)): pass
Alex Martelli
sorry, I simplified too much, it is only the side effects of predicate() that I need. I also tried an `if limit` as suggested but felt I was repeating code in the branches.
msw
A: 

What you want to do seems perfectly suited for a while loop:

def ello_bruce(limit=None):
    max = 10**5
    # if you consider 0 to be an invalid value for limit you can also do
    # if limit:
    if limit is None: 
        limit = max

    while max and limit:
        if predicate(i):
            limit -= 1
        max -=1

The loop stops if either max or limit reaches zero.

Felix Kling
+1  A: 

You should remove the nested ifs:

if predicate(i) and not limit is None:
    ...
codeape
A: 

Um. As far as I understand it, predicate just computes in segments, and you totally ignore its return value, right?

This is another take:

import itertools

def ello_bruce(limit=None):
    if limit is None:
        limiter= itertools.repeat(None)
    else:
        limiter= xrange(limit)

    # since predicate is a Python function
    # itertools looping won't be faster, so use plain for.
    # remember to replace the xrange(100000) with your own iterator
    for dummy in itertools.izip(xrange(100000), limiter):
        pass

Also, remove the unneeded return True from the end of predicate.

ΤΖΩΤΖΙΟΥ