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

Generator that produces floats, equivalent to range for integers, minimising rounding errors by using only a single multiplication and addition for each number, and no divisions.

This generator takes an optional argument controlling whether it produces numbers from the open, closed, or half-open interval.

Python, 55 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
51
52
53
54
55
def frange(*args):
    """frange([start, ] end [, step [, mode]]) -> generator

    A float range generator. If not specified, the default start is 0.0
    and the default step is 1.0.

    Optional argument mode sets whether frange outputs an open or closed
    interval. 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.
    """
    mode = 1  # Default mode is half-open.
    n = len(args)
    if n == 1:
        args = (0.0, args[0], 1.0)
    elif n == 2:
        args = args + (1.0,)
    elif n == 4:
        mode = args[3]
        args = args[0:3]
    elif n != 3:
        raise TypeError('frange expects 1-4 arguments, got %d' % n)
    assert len(args) == 3
    try:
        start, end, step = [a + 0.0 for a in args]
    except TypeError:
        raise TypeError('arguments must be numbers')
    if step == 0.0:
        raise ValueError('step must not be zero')
    if not isinstance(mode, int):
        raise TypeError('mode must be an int')
    if mode & 1:
        i, x = 0, start
    else:
        i, x = 1, start+step
    if step > 0:
        if mode & 2:
            from operator import le as comp
        else:
            from operator import lt as comp
    else:
        if mode & 2:
            from operator import ge as comp
        else:
            from operator import gt as comp
    while comp(x, end):
        yield x
        i += 1
        x = start + i*step

The built-in range function produces ints in the half-open interval, with the start point included and the end excluded. For floating point ranges, it is useful to control whether the start and end points are included:

>>> list(frange(0.0, 1.0, 0.25, 0))  # open-interval
[0.25, 0.5, 0.75]
>>> list(frange(0.0, 1.0, 0.25, 1))  # half-open at end (the default)
[0.0, 0.25, 0.5, 0.75]
>>> list(frange(0.0, 1.0, 0.25, 2))  # half-open at start
[0.25, 0.5, 0.75, 1.0]
>>> list(frange(0.0, 1.0, 0.25, 3))  # closed-interval
[0.0, 0.25, 0.5, 0.75, 1.0]

7 comments

Sunjay Varma 13 years, 8 months ago  # | flag

GREAT RECIPE!

Guido van Rossum 12 years, 6 months ago  # | flag

This recipe gives unexpected results, e.g. frange(0.0, 2.1, 0.7) returns [0.0, 0.7, 1.4, 2.0999999999999996] -- the final value is unexpected.

Steven D'Aprano (author) 12 years, 6 months ago  # | flag

To add further to Guido's comment, this recipe doesn't remove all floating point rounding issues. In general, floating point numbers don't necessarily sum to the exact value you might expect:

>>> 3*0.7 == 2.1
False

So you need to think carefully about the values you use, in order to get an acceptable amount of surprise:

>>> list(frange(0, 2.1, 0.7))
[0.0, 0.7, 1.4, 2.0999999999999996]
>>> list(frange(0, 2.1, 2.1/3))
[0.0, 0.7000000000000001, 1.4000000000000001]

Or don't use floats at all:

>>> from fractions import Fraction
>>> [Fraction(i)/7 for i in range(3)]
[Fraction(0, 1), Fraction(1, 7), Fraction(2, 7)]
mimi.vx 12 years, 5 months ago  # | flag

or use Decimal type ..

Steven D'Aprano (author) 12 years, 5 months ago  # | flag

Despite what many people think, Decimal still suffers from the same sort of rounding issues as float. The only differences are that Decimal is base 10 instead of base 2, and that Decimal's precision is configurable.

>>> x = Decimal(1)/3
>>> 3*x == 1
False
Diogo Baeder 12 years, 5 months ago  # | flag
Laurent Pointal 12 years, 4 months ago  # | flag

I wrote mine here (source is too long for ActiveState comment, see at link): http://perso.limsi.fr/pointal/python:floatrange

An example of usage:

>>> from floatrange import floatrange
>>> floatrange(5)
floatrange(0.0, 5.0, 1.0)
>>> list(floatrange(5))
[0.0, 1.0, 2.0, 3.0, 4.0]
>>> list(floatrange(3.2,5.4,0.2))
[3.2, 3.4000000000000004, 3.6, 3.8000000000000003, 4.0, 4.2, 4.4, 4.6000000000000005, 4.800000000000001, 5.0, 5.2]
>>> 6 in floatrange(1,8)
True
>>> 6.1 in floatrange(1,8,1,prec=0.2)
True
>>> 6.1 in floatrange(1,8,1,prec=0.05)
False
>>> list(reversed(floatrange(5)))
[4.0, 3.0, 2.0, 1.0, 0.0]
>>> list(floatrange(10.1,9.7,-0.1))
[10.1, 10.0, 9.9, 9.799999999999999]