Welcome, guest | Sign In | My Account | Store | Cart

Generating a list of equally-spaced floats can surprising due to floating point rounding. See, for example, the recipe for a floating point range. One way of avoiding some surprises is by changing the API: instead of specifying a start, stop and step values, instead use a start, stop and count:

>>> list(spread(0.0, 2.1, 7))
[0.0, 0.3, 0.6, 0.9, 1.2, 1.5, 1.8]

Like frange spread takes an optional mode argument to select whether the start and end values are included. By default, start is included and end is not, and exactly count values are returned.

Python, 50 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from fractions import Fraction

def spread(start, end, count, mode=1):
    """spread(start, end, count [, mode]) -> generator

    Yield a sequence of evenly-spaced numbers between start and end.

    The range start...end is divided into count evenly-spaced (or as close to
    evenly-spaced as possible) intervals. The end-points of each interval are
    then yielded, optionally including or excluding start and end themselves.
    By default, start is included and end is excluded.

    For example, with start=0, end=2.1 and count=3, the range is divided into
    three intervals:

        (0.0)-----(0.7)-----(1.4)-----(2.1)

    resulting in:

        >>> list(spread(0.0, 2.1, 3))
        [0.0, 0.7, 1.4]

    Optional argument mode controls whether spread() includes the start and
    end values. mode must be an int. Bit zero of mode controls whether start
    is included (on) or excluded (off); bit one does the same for end. Hence:

        0 -> open interval (start and end both excluded)
        1 -> half-open (start included, end excluded)
        2 -> half open (start excluded, end included)
        3 -> closed (start and end both included)

    By default, mode=1 and only start is included in the output.

    (Note: depending on mode, the number of values returned can be count,
    count-1 or count+1.)
    """
    if not isinstance(mode, int):
        raise TypeError('mode must be an int')
    if count != int(count):
        raise ValueError('count must be an integer')
    if count <= 0:
        raise ValueError('count must be positive')
    if mode & 1:
        yield start
    width = Fraction(end-start)
    start = Fraction(start)
    for i in range(1, count):
        yield float(start + i*width/count)
    if mode & 2:
        yield end

One subtlety is that count is not the number of points returned, but the number of divisions in the range. The number of points yielded will be count-1 if you exclude both the start and end points, and count+1 if you include them both. This is just the usual fence-post problem, although in this case it should not be considered an error.

Another solution to this problem, although one with a different API, is linspace from numpy. But note that it too can be surprising (although not as surprising as frange). Results may look exact when printed, but may not necessarily be the exact value you expect:

>>> import numpy as np  # (using Python 2.5)
>>> np.linspace(0.0, 2.1, 8, True)
array([ 0. ,  0.3,  0.6,  0.9,  1.2,  1.5,  1.8,  2.1])
>>>
>>> exact = (0.0, 0.3, 0.6, 0.9, 1.2, 1.5, 1.8, 2.1)
>>> [a-b for (a,b) in zip(np.linspace(0.0, 2.1, 8, True), exact)]
[0.0, 0.0, 0.0, -1.1102230246251565e-16, 0.0, 0.0, -2.2204460492503131e-16, 0.0]

In this case, spread manages to do slightly better:

>>> [a-b for (a,b) in zip(spread(0.0, 2.1, 7, mode=3), exact)]
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]

although that will not necessarily always be the case.