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]
``````

Sunjay Varma 13 years, 9 months ago

GREAT RECIPE!

Guido van Rossum 12 years, 7 months ago

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, 7 months ago

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, 7 months ago

or use Decimal type ..

Steven D'Aprano (author) 12 years, 7 months ago

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, 7 months ago
Laurent Pointal 12 years, 5 months ago

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]
``````
 Created by Steven D'Aprano on Wed, 24 Feb 2010 (MIT)