I l@ve RuBoard Previous Section Next Section

1.16 Spanning a Range Defined by Floats

Credit: Dinu C. Gherman, Paul M. Winkler

1.16.1 Problem

You need an arithmetic progression, just like the built-in function range, but with float values (range works only on integers).

1.16.2 Solution

Although this functionality is not available as a built-in, it's not hard to code it with a loop:

def frange(start, end=None, inc=1.0):
    "A range-like function that does accept float increments..."

    if end == None:
        end = start + 0.0     # Ensure a float value for 'end'
        start = 0.0
    assert inc                # sanity check

    L = []
    while 1:
        next = start + len(L) * inc
        if inc > 0 and next >= end:
            break
        elif inc < 0 and next <= end:
            break
        L.append(next)

    return L

1.16.3 Discussion

Sadly missing in the Python standard library, the function in this recipe lets you use ranges, just as with the built-in function range, but with float arguments.

Many theoretical restrictions apply, but this function is more useful in practice than in theory. People who work with floating-point numbers all the time have many war stories about billion-dollar projects that failed because someone did not take into consideration the strange things that modern hardware does when comparing floating-point numbers. But for pedestrian cases, simple approaches like this recipe generally work.

You can get a substantial speed boost by preallocating the list instead of calling append repeatedly. This also allows you to get rid of the conditionals in the inner loop. For one element, this version is barely faster, but with more than 10 elements it's consistently about 5 times faster�the kind of performance ratio that is worth caring about. I get identical output for every test case I can think of:

def frange2(start, end=None, inc=1.0):
    "A faster range-like function that does accept float increments..."
    if end == None:
        end = start + 0.0
        start = 0.0
    else: start += 0.0 # force it to be a float

    count = int((end - start) / inc)
    if start + count * inc != end:
        # Need to adjust the count. AFAICT, it always comes up one short.
        count += 1

    L = [start] * count
    for i in xrange(1, count):
        L[i] = start + i * inc

    return L

Both versions rely on a single multiplication and one addition to compute each item, to avoid accumulating error by repeated additions. This is why, for example, the body of the for loop in frange2 is not:

L[i] = L[i-1] + inc

In Python 2.2, if all you need to do is loop on the result of frange, you can save some memory by turning this function into a simple generator, yielding an iterator when you call it:

from _ _future_ _ import generators

def frangei(start, end=None, inc=1.0):
    "An xrange-like simple generator that does accept float increments..."

    if end == None:
        end = start + 0.0
        start = 0.0
    assert inc                # sanity check

    i = 0
    while 1:
        next = start + i * inc
        if inc > 0 and next >= end:
            break
        elif inc < 0 and next <= end:
            break
        yield next
        i += 1

If you use this recipe a lot, you should probably take a look at Numeric Python and other third-party packages that take computing with floating-point numbers seriously. This recipe, for example, will not scale well to very large ranges, while those defined in Numeric Python will.

1.16.4 See Also

Documentation for the range built-in function in the Library Reference; Numeric Python (http://www.pfdubois.com/numpy/).

    I l@ve RuBoard Previous Section Next Section