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.
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]
GREAT RECIPE!
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.
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:
So you need to think carefully about the values you use, in order to get an acceptable amount of surprise:
Or don't use floats at all:
or use Decimal type ..
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.
My attempt: https://gist.github.com/1239977
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: