range() for floats
Asked Answered
P

25

234

Is there a range() equivalent for floats in Python?

>>> range(0.5,5,1.5)
[0, 1, 2, 3, 4]
>>> range(0.5,5,0.5)

Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    range(0.5,5,0.5)
ValueError: range() step argument must not be zero
Portecochere answered 1/9, 2011 at 7:30 Comment(8)
Those aren't fractions but floats. And floats are... well, likely to give different results than you expect.Supersensitive
A quick workaround would be to treat integers as decimals, eg: range(5, 50, 5), and then just divide every number by 10.Wholehearted
@delnan - updated. I'm willing to accept minute inaccuracies for the convenience of having a float rangePortecochere
possible duplicate of Python decimal range() step valuePortecochere
@Wholehearted - this is just an example - the real code is of course parametric :)Portecochere
@Jonathan Nothing is stopping you from writing a wrapper for range() :)Wholehearted
You'll find much more insight into the surprising variety of good and bad answers to this question from the Python decimal range() step value question, and the frange(), a range function with float increments (ActiveState Code) it refers to.Abib
Possible duplicate of How to use a decimal range() step value?Assist
S
114
I don't know a built-in function, but writing one like [this](https://mcmap.net/q/53195/-how-do-i-use-a-decimal-step-value-for-range) shouldn't be too complicated.
def frange(x, y, jump):
  while x < y:
    yield x
    x += jump
---

As the comments mention, this could produce unpredictable results like:

>>> list(frange(0, 100, 0.1))[-1]
99.9999999999986

To get the expected result, you can use one of the other answers in this question, or as @Tadhg mentioned, you can use decimal.Decimal as the jump argument. Make sure to initialize it with a string rather than a float.

>>> import decimal
>>> list(frange(0, 100, decimal.Decimal('0.1')))[-1]
Decimal('99.9')

Or even:

import decimal

def drange(x, y, jump):
  while x < y:
    yield float(x)
    x += decimal.Decimal(jump)

And then:

>>> list(drange(0, 100, '0.1'))[-1]
99.9

[editor's not: if you only use positive jump and integer start and stop (x and y) , this works fine. For a more general solution see here.]

Survive answered 1/9, 2011 at 7:36 Comment(13)
Python's motto is actually There should be one-- and preferably only one --obvious way to do it. But Python's awesome anyways :)Portecochere
>>> print list(frange(0,100,0.1))[-1]==100.0 will be FalseEpizoic
frange can work unexpectedly. Due to the curse of floating point arithmetics, for example frange(0.0, 1.0, 0.1) yields 11 values, where the last value is 0.9999999999999999. A practical improvement would be while x + sys.float_info.epsilon < y: although even this can probably fail with large numbers.Collect
After the previous comment, I quickly found out that the use of sys.float_info.epsilon is not enough because the summation accumulates the error. The multiplication of jump is a way to avoid that. See my answer for a robust solution.Collect
-1 Please do not use this code, at least not in software that might ever affect my life. There is no way to make it work reliably. Don't use Akseli Palén's answer either. Use Xaerxess's or wim's answer (except ignore the part about arange).Involute
this works great if you use decimal.Decimal as step instead of floats.Ingram
As shown it is currently broken if 'x passed to frange is a float value. I suggested changing the last line of frange to x = decimal.Decimal(str(x)) + decimal.Decimal(str(jump)) and removing the quotes from the jump in a call: frange(0.1, 100, 0.25).Ptolemaic
That same code would also beFalse for list(range(0,100))[-1]==100.0 ; the last number would be 99, not 100 because of zero indexing. I guess the question here is around expected behavior, should range of values be inclusive of the max value given that we don't use floats for indexing? My initial assessment was no, but I can see arguments for both sides. @VolodimirKopeyBattaglia
@benrg, that's a bit overstated. The implementation as given is unreliable. But it is possible to make it work reliably, see seq in R or : in Matlab.Myrlmyrle
@a-donda When I wrote that comment, this answer was just the part at the beginning that's now struck out. I think the answer is okay now. The reason I said frange can't be made reliable is that there's no objectively correct way to decide whether the caller wants a step size of 1/3 or the 0.333333333333333314829616256247390992939472198486328125 that was actually passed. It's up to the caller to use it safely, and it misbehaves silently when it misbehaves. I don't think R's solution of using a fuzzy comparison with a hardcoded relative precision of 10^{-10} qualifies as reliable.Involute
drange as defined here requires the start and end parameters to be ints, not floats. If you try to do list(drange(0.5, 3.5, '0.3')), you will get "TypeError: unsupported operand type(s) for +=: 'float' and 'decimal.Decimal'". Best to make the start and end decimals too.Maestricht
This answer is a confusing mess. A much better answer to the OP's question is: Yes: numpy.linspace().Jana
Above-mentioned code is incomplete because I'm getting an error: NameError: name 'frange' is not definedSubterrane
L
166

You can either use:

[x / 10.0 for x in range(5, 50, 15)]

or use lambda / map:

map(lambda x: x/10.0, range(5, 50, 15))
Loathing answered 1/9, 2011 at 7:36 Comment(4)
And array(range(5,50,15)) / 10.0 as numpy arrays have operators for handling division, multiplication and so onColb
@edvaldig: you're right, I didn't know about this... Nevertheless I think arange(0.5, 5, 1.5) is IMO more readable.Hygro
I prefer this answer over the accepted one, because the first two solutions presented are based on iterating over integers and deriving the final floats from the integers. This is more robust. If you do it directly with floats, you risk having strange one-off errors due to how floats are represented internally. For instance, if you try list(frange(0, 1, 0.5)), it works fine and 1 is excluded, but if you try list(frange(0, 1, 0.1)), the last value you get is close to 1.0, which is probably not what you want. The solutions presented here don't have this problem.Airman
Never use numpy.arange (the numpy documentation itself recommends against it). Use numpy.linspace as recommended by wim, or one of the other suggestions in this answer.Involute
S
114
I don't know a built-in function, but writing one like [this](https://mcmap.net/q/53195/-how-do-i-use-a-decimal-step-value-for-range) shouldn't be too complicated.
def frange(x, y, jump):
  while x < y:
    yield x
    x += jump
---

As the comments mention, this could produce unpredictable results like:

>>> list(frange(0, 100, 0.1))[-1]
99.9999999999986

To get the expected result, you can use one of the other answers in this question, or as @Tadhg mentioned, you can use decimal.Decimal as the jump argument. Make sure to initialize it with a string rather than a float.

>>> import decimal
>>> list(frange(0, 100, decimal.Decimal('0.1')))[-1]
Decimal('99.9')

Or even:

import decimal

def drange(x, y, jump):
  while x < y:
    yield float(x)
    x += decimal.Decimal(jump)

And then:

>>> list(drange(0, 100, '0.1'))[-1]
99.9

[editor's not: if you only use positive jump and integer start and stop (x and y) , this works fine. For a more general solution see here.]

Survive answered 1/9, 2011 at 7:36 Comment(13)
Python's motto is actually There should be one-- and preferably only one --obvious way to do it. But Python's awesome anyways :)Portecochere
>>> print list(frange(0,100,0.1))[-1]==100.0 will be FalseEpizoic
frange can work unexpectedly. Due to the curse of floating point arithmetics, for example frange(0.0, 1.0, 0.1) yields 11 values, where the last value is 0.9999999999999999. A practical improvement would be while x + sys.float_info.epsilon < y: although even this can probably fail with large numbers.Collect
After the previous comment, I quickly found out that the use of sys.float_info.epsilon is not enough because the summation accumulates the error. The multiplication of jump is a way to avoid that. See my answer for a robust solution.Collect
-1 Please do not use this code, at least not in software that might ever affect my life. There is no way to make it work reliably. Don't use Akseli Palén's answer either. Use Xaerxess's or wim's answer (except ignore the part about arange).Involute
this works great if you use decimal.Decimal as step instead of floats.Ingram
As shown it is currently broken if 'x passed to frange is a float value. I suggested changing the last line of frange to x = decimal.Decimal(str(x)) + decimal.Decimal(str(jump)) and removing the quotes from the jump in a call: frange(0.1, 100, 0.25).Ptolemaic
That same code would also beFalse for list(range(0,100))[-1]==100.0 ; the last number would be 99, not 100 because of zero indexing. I guess the question here is around expected behavior, should range of values be inclusive of the max value given that we don't use floats for indexing? My initial assessment was no, but I can see arguments for both sides. @VolodimirKopeyBattaglia
@benrg, that's a bit overstated. The implementation as given is unreliable. But it is possible to make it work reliably, see seq in R or : in Matlab.Myrlmyrle
@a-donda When I wrote that comment, this answer was just the part at the beginning that's now struck out. I think the answer is okay now. The reason I said frange can't be made reliable is that there's no objectively correct way to decide whether the caller wants a step size of 1/3 or the 0.333333333333333314829616256247390992939472198486328125 that was actually passed. It's up to the caller to use it safely, and it misbehaves silently when it misbehaves. I don't think R's solution of using a fuzzy comparison with a hardcoded relative precision of 10^{-10} qualifies as reliable.Involute
drange as defined here requires the start and end parameters to be ints, not floats. If you try to do list(drange(0.5, 3.5, '0.3')), you will get "TypeError: unsupported operand type(s) for +=: 'float' and 'decimal.Decimal'". Best to make the start and end decimals too.Maestricht
This answer is a confusing mess. A much better answer to the OP's question is: Yes: numpy.linspace().Jana
Above-mentioned code is incomplete because I'm getting an error: NameError: name 'frange' is not definedSubterrane
A
110

I used to use numpy.arange but had some complications controlling the number of elements it returns, due to floating point errors. So now I use linspace, e.g.:

>>> import numpy
>>> numpy.linspace(0, 10, num=4)
array([  0.        ,   3.33333333,   6.66666667,  10.        ])
Arbiter answered 1/9, 2011 at 8:30 Comment(4)
There's still floating point errors though, whithout the use of decimal, e.g.: np.linspace(-.1,10,num=5050)[0] Treece
@Treece No, that's not an error. You will find np.linspace(-.1,10,num=5050)[0] == -.1 is True. It's just that the repr(np.float64('-0.1')) shows more digits.Arbiter
While that particular example shows no excess rounding error, there are failure cases. For example, print(numpy.linspace(0, 3, 148)[49]) prints 0.9999999999999999 when the ideal result would be 1.0. linspace does a much better job than arange, but it is not guaranteed to produce the minimum possible rounding error.Anuradhapura
It is guaranteed to perform correct endpoint handling, and always produce exactly the requested number of elements.Anuradhapura
S
41

Pylab has frange (a wrapper, actually, for matplotlib.mlab.frange):

>>> import pylab as pl
>>> pl.frange(0.5,5,0.5)
array([ 0.5,  1. ,  1.5,  2. ,  2.5,  3. ,  3.5,  4. ,  4.5,  5. ])
Sludge answered 1/2, 2013 at 19:7 Comment(1)
Frange is deprecated since matplotlib version 2.2. numpy.arange should be used.Largess
A
16

Eagerly evaluated (2.x range):

[x * .5 for x in range(10)]

Lazily evaluated (2.x xrange, 3.x range):

itertools.imap(lambda x: x * .5, xrange(10)) # or range(10) as appropriate

Alternately:

itertools.islice(itertools.imap(lambda x: x * .5, itertools.count()), 10)
# without applying the `islice`, we get an infinite stream of half-integers.
Arvy answered 1/9, 2011 at 7:40 Comment(1)
+1; but why not (x * .5 for x in range(10)) as a generator expression for lazy evaluation?Everett
R
12

using itertools: lazily evaluated floating point range:

>>> from itertools import count, takewhile
>>> def frange(start, stop, step):
        return takewhile(lambda x: x< stop, count(start, step))

>>> list(frange(0.5, 5, 1.5))
# [0.5, 2.0, 3.5]
Reredos answered 6/12, 2015 at 7:7 Comment(1)
+1 for using itertools.takewhile. However, itertools.count(start, step) suffers from accumulated floating-point errors. (Evaluate takewhile(lambda x: x < 100, count(0, 0.1)) for example.) I would write takewhile(lambda x: x < stop, (start + i * step for i in count())) instead.Aflame
J
7

I helped add the function numeric_range to the package more-itertools.

more_itertools.numeric_range(start, stop, step) acts like the built in function range but can handle floats, Decimal, and Fraction types.

>>> from more_itertools import numeric_range
>>> tuple(numeric_range(.1, 5, 1))
(0.1, 1.1, 2.1, 3.1, 4.1)
Jumble answered 22/4, 2017 at 18:28 Comment(0)
P
6

A solution without numpy etc dependencies was provided by kichik but due to the floating point arithmetics, it often behaves unexpectedly. As noted by me and blubberdiblub, additional elements easily sneak into the result. For example naive_frange(0.0, 1.0, 0.1) would yield 0.999... as its last value and thus yield 11 values in total.

A bit more robust version is provided here:

def frange(x, y, jump=1.0):
    '''Range for floats.'''
    i = 0.0
    x = float(x)  # Prevent yielding integers.
    x0 = x
    epsilon = jump / 2.0
    yield x  # yield always first value
    while x + epsilon < y:
        i += 1.0
        x = x0 + i * jump
        if x < y:
          yield x

Because the multiplication, the rounding errors do not accumulate. The use of epsilon takes care of possible rounding error of the multiplication, even though issues of course might rise in the very small and very large ends. Now, as expected:

> a = list(frange(0.0, 1.0, 0.1))
> a[-1]
0.9
> len(a)
10

And with somewhat larger numbers:

> b = list(frange(0.0, 1000000.0, 0.1))
> b[-1]
999999.9
> len(b)
10000000

The code is also available as a GitHub Gist.

Provision answered 18/3, 2016 at 18:19 Comment(6)
This fails with frange(2.0, 17.0/6.0, 1.0/6.0). There is no way it can ever be made robust.Involute
@Involute Thanks for pointing this out! It led me to realize that the epsilon should depend on the jump, so I reviewed the algorithm and repaired the issue. This new version is much more robust, isn't it?Collect
This fails for frange(0.026, 0.619, 0.078).Maggi
@Maggi Thanks for noticing. I fixed the issue. Somehow I did not consider the case where y - x is not a multiple of the jump. How could I been so blind? Well, the fix is to ensure x is less or equal to y before yield. Now frange(0.026, 0.619, 0.078) yields 0.026, 0.104, 0.182, 0.26, 0.338, 0.416, 0.494, 0.572 as it should be.Collect
I don't see the condition in the code. Also, check the case frange(.071,.493,.001) which should not end with 0.493. But if you think that is because of the emit when x <= y you can change that to x < y but then frange(0.569, 0.799, 0.23) will fail because it emits more than 0.569. I am testing code against the version that I presented.Maggi
@Maggi Oh, I see. Thanks for the good eyes. I changed <= to <. I guess it is hopeless to try to improve this any further. The base problem stays the same: 0.569 + 0.23 > 0.799 is a true statement in Python and it is rightfully so, although for our base-10 accustomed eyes it looks like false. Using decimal.Decimal is probably the only right way to do this, as presented in the accepted answerCollect
P
6

As kichik wrote, this shouldn't be too complicated. However this code:

def frange(x, y, jump):
  while x < y:
    yield x
    x += jump

Is inappropriate because of the cumulative effect of errors when working with floats. That is why you receive something like:

>>>list(frange(0, 100, 0.1))[-1]
99.9999999999986

While the expected behavior would be:

>>>list(frange(0, 100, 0.1))[-1]
99.9

Solution 1

The cumulative error can simply be reduced by using an index variable. Here's the example:

from math import ceil

    def frange2(start, stop, step):
        n_items = int(ceil((stop - start) / step))
        return (start + i*step for i in range(n_items))

This example works as expected.

Solution 2

No nested functions. Only a while and a counter variable:

def frange3(start, stop, step):
    res, n = start, 1

    while res < stop:
        yield res
        res = start + n * step
        n += 1

This function will work well too, except for the cases when you want the reversed range. E.g:

>>>list(frange3(1, 0, -.1))
[]

Solution 1 in this case will work as expected. To make this function work in such situations, you must apply a hack, similar to the following:

from operator import gt, lt

def frange3(start, stop, step):
    res, n = start, 0.
    predicate = lt if start < stop else gt
    while predicate(res, stop):
        yield res
        res = start + n * step
        n += 1

With this hack you can use these functions with negative steps:

>>>list(frange3(1, 0, -.1))
[1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.3999999999999999, 0.29999999999999993, 0.19999999999999996, 0.09999999999999998]

Solution 3

You can go even further with plain standard library and compose a range function for the most of numeric types:

from itertools import count
from itertools import takewhile

def any_range(start, stop, step):
    start = type(start + step)(start)
    return takewhile(lambda n: n < stop, count(start, step))

This generator is adapted from the Fluent Python book (Chapter 14. Iterables, Iterators and generators). It will not work with decreasing ranges. You must apply a hack, like in the previous solution.

You can use this generator as follows, for example:

>>>list(any_range(Fraction(2, 1), Fraction(100, 1), Fraction(1, 3)))[-1]
299/3
>>>list(any_range(Decimal('2.'), Decimal('4.'), Decimal('.3')))
[Decimal('2'), Decimal('2.3'), Decimal('2.6'), Decimal('2.9'), Decimal('3.2'), Decimal('3.5'), Decimal('3.8')]

And of course you can use it with float and int as well.

Be careful

If you want to use these functions with negative steps, you should add a check for the step sign, e.g.:

no_proceed = (start < stop and step < 0) or (start > stop and step > 0)
if no_proceed: raise StopIteration

The best option here is to raise StopIteration, if you want to mimic the range function itself.

Mimic range

If you would like to mimic the range function interface, you can provide some argument checks:

def any_range2(*args):
    if len(args) == 1:
        start, stop, step = 0, args[0], 1.
    elif len(args) == 2:
        start, stop, step = args[0], args[1], 1.
    elif len(args) == 3:
        start, stop, step = args
    else:
        raise TypeError('any_range2() requires 1-3 numeric arguments')

    # here you can check for isinstance numbers.Real or use more specific ABC or whatever ...

    start = type(start + step)(start)
    return takewhile(lambda n: n < stop, count(start, step))

I think, you've got the point. You can go with any of these functions (except the very first one) and all you need for them is python standard library.

Preset answered 5/4, 2020 at 6:49 Comment(0)
A
6

I do not know if the question is old but there is a arange function in the NumPy library, it could work as a range.

np.arange(0,1,0.1)

#out: 

array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
Autophyte answered 6/9, 2021 at 14:32 Comment(0)
W
5

There is no such built-in function, but you can use the following (Python 3 code) to do the job as safe as Python allows you to.

from fractions import Fraction

def frange(start, stop, jump, end=False, via_str=False):
    """
    Equivalent of Python 3 range for decimal numbers.

    Notice that, because of arithmetic errors, it is safest to
    pass the arguments as strings, so they can be interpreted to exact fractions.

    >>> assert Fraction('1.1') - Fraction(11, 10) == 0.0
    >>> assert Fraction( 0.1 ) - Fraction(1, 10) == Fraction(1, 180143985094819840)

    Parameter `via_str` can be set to True to transform inputs in strings and then to fractions.
    When inputs are all non-periodic (in base 10), even if decimal, this method is safe as long
    as approximation happens beyond the decimal digits that Python uses for printing.


    For example, in the case of 0.1, this is the case:

    >>> assert str(0.1) == '0.1'
    >>> assert '%.50f' % 0.1 == '0.10000000000000000555111512312578270211815834045410'


    If you are not sure whether your decimal inputs all have this property, you are better off
    passing them as strings. String representations can be in integer, decimal, exponential or
    even fraction notation.

    >>> assert list(frange(1, 100.0, '0.1', end=True))[-1] == 100.0
    >>> assert list(frange(1.0, '100', '1/10', end=True))[-1] == 100.0
    >>> assert list(frange('1', '100.0', '.1', end=True))[-1] == 100.0
    >>> assert list(frange('1.0', 100, '1e-1', end=True))[-1] == 100.0
    >>> assert list(frange(1, 100.0, 0.1, end=True))[-1] != 100.0
    >>> assert list(frange(1, 100.0, 0.1, end=True, via_str=True))[-1] == 100.0

    """
    if via_str:
        start = str(start)
        stop = str(stop)
        jump = str(jump)
    start = Fraction(start)
    stop = Fraction(stop)
    jump = Fraction(jump)
    while start < stop:
        yield float(start)
        start += jump
    if end and start == stop:
        yield(float(start))

You can verify all of it by running a few assertions:

assert Fraction('1.1') - Fraction(11, 10) == 0.0
assert Fraction( 0.1 ) - Fraction(1, 10) == Fraction(1, 180143985094819840)

assert str(0.1) == '0.1'
assert '%.50f' % 0.1 == '0.10000000000000000555111512312578270211815834045410'

assert list(frange(1, 100.0, '0.1', end=True))[-1] == 100.0
assert list(frange(1.0, '100', '1/10', end=True))[-1] == 100.0
assert list(frange('1', '100.0', '.1', end=True))[-1] == 100.0
assert list(frange('1.0', 100, '1e-1', end=True))[-1] == 100.0
assert list(frange(1, 100.0, 0.1, end=True))[-1] != 100.0
assert list(frange(1, 100.0, 0.1, end=True, via_str=True))[-1] == 100.0

assert list(frange(2, 3, '1/6', end=True))[-1] == 3.0
assert list(frange(0, 100, '1/3', end=True))[-1] == 100.0

Code available on GitHub

Wildon answered 22/12, 2016 at 2:17 Comment(0)
B
5

Why Is There No Floating Point Range Implementation In The Standard Library?

As made clear by all the posts here, there is no floating point version of range(). That said, the omission makes sense if we consider that the range() function is often used as an index (and of course, that means an accessor) generator. So, when we call range(0,40), we're in effect saying we want 40 values starting at 0, up to 40, but non-inclusive of 40 itself.

When we consider that index generation is as much about the number of indices as it is their values, the use of a float implementation of range() in the standard library makes less sense. For example, if we called the function frange(0, 10, 0.25), we would expect both 0 and 10 to be included, but that would yield a generator with 41 values, not the 40 one might expect from 10/0.25.

Thus, depending on its use, an frange() function will always exhibit counter intuitive behavior; it either has too many values as perceived from the indexing perspective or is not inclusive of a number that reasonably should be returned from the mathematical perspective. In other words, it's easy to see how such a function would appear to conflate two very different use cases – the naming implies the indexing use case; the behavior implies a mathematical one.

The Mathematical Use Case

With that said, as discussed in other posts, numpy.linspace() performs the generation from the mathematical perspective nicely:

numpy.linspace(0, 10, 41)
array([  0.  ,   0.25,   0.5 ,   0.75,   1.  ,   1.25,   1.5 ,   1.75,
         2.  ,   2.25,   2.5 ,   2.75,   3.  ,   3.25,   3.5 ,   3.75,
         4.  ,   4.25,   4.5 ,   4.75,   5.  ,   5.25,   5.5 ,   5.75,
         6.  ,   6.25,   6.5 ,   6.75,   7.  ,   7.25,   7.5 ,   7.75,
         8.  ,   8.25,   8.5 ,   8.75,   9.  ,   9.25,   9.5 ,   9.75,  10.
])

The Indexing Use Case

And for the indexing perspective, I've written a slightly different approach with some tricksy string magic that allows us to specify the number of decimal places.

# Float range function - string formatting method
def frange_S (start, stop, skip = 1.0, decimals = 2):
    for i in range(int(start / skip), int(stop / skip)):
        yield float(("%0." + str(decimals) + "f") % (i * skip))

Similarly, we can also use the built-in round function and specify the number of decimals:

# Float range function - rounding method
def frange_R (start, stop, skip = 1.0, decimals = 2):
    for i in range(int(start / skip), int(stop / skip)):
        yield round(i * skip, ndigits = decimals)

A Quick Comparison & Performance

Of course, given the above discussion, these functions have a fairly limited use case. Nonetheless, here's a quick comparison:

def compare_methods (start, stop, skip):

    string_test  = frange_S(start, stop, skip)
    round_test   = frange_R(start, stop, skip)

    for s, r in zip(string_test, round_test):
        print(s, r)

compare_methods(-2, 10, 1/3)

The results are identical for each:

-2.0 -2.0
-1.67 -1.67
-1.33 -1.33
-1.0 -1.0
-0.67 -0.67
-0.33 -0.33
0.0 0.0
...
8.0 8.0
8.33 8.33
8.67 8.67
9.0 9.0
9.33 9.33
9.67 9.67

And some timings:

>>> import timeit

>>> setup = """
... def frange_s (start, stop, skip = 1.0, decimals = 2):
...     for i in range(int(start / skip), int(stop / skip)):
...         yield float(("%0." + str(decimals) + "f") % (i * skip))
... def frange_r (start, stop, skip = 1.0, decimals = 2):
...     for i in range(int(start / skip), int(stop / skip)):
...         yield round(i * skip, ndigits = decimals)
... start, stop, skip = -1, 8, 1/3
... """

>>> min(timeit.Timer('string_test = frange_s(start, stop, skip); [x for x in string_test]', setup=setup).repeat(30, 1000))
0.024284090992296115

>>> min(timeit.Timer('round_test = frange_r(start, stop, skip); [x for x in round_test]', setup=setup).repeat(30, 1000))
0.025324633985292166

Looks like the string formatting method wins by a hair on my system.

The Limitations

And finally, a demonstration of the point from the discussion above and one last limitation:

# "Missing" the last value (10.0)
for x in frange_R(0, 10, 0.25):
    print(x)

0.25
0.5
0.75
1.0
...
9.0
9.25
9.5
9.75

Further, when the skip parameter is not divisible by the stop value, there can be a yawning gap given the latter issue:

# Clearly we know that 10 - 9.43 is equal to 0.57
for x in frange_R(0, 10, 3/7):
    print(x)

0.0
0.43
0.86
1.29
...
8.14
8.57
9.0
9.43

There are ways to address this issue, but at the end of the day, the best approach would probably be to just use Numpy.

Battaglia answered 18/12, 2017 at 23:4 Comment(2)
This is quite a twisted argument. range() should simply be looked at iteration generator and whether it is used in for loop or to index something should be left to callers. People have been using floats in for loop for millenia and above justifications are nonsensical. People in Python committees screwed up here big time and good argument probably got drowned out by some twisted justifications like above. It's that plain and simple. There are now too many decisions like above enshrined in Python language.Crenshaw
The first value from the function should be the starting point if there are any points at all to be returned; list(frange_S(2,3,4)) is [] but should be [2.0]Maggi
B
3

A simpler library-less version

Aw, heck -- I'll toss in a simple library-less version. Feel free to improve on it[*]:

def frange(start=0, stop=1, jump=0.1):
    nsteps = int((stop-start)/jump)
    dy = stop-start
    # f(i) goes from start to stop as i goes from 0 to nsteps
    return [start + float(i)*dy/nsteps for i in range(nsteps)]

The core idea is that nsteps is the number of steps to get you from start to stop and range(nsteps) always emits integers so there's no loss of accuracy. The final step is to map [0..nsteps] linearly onto [start..stop].

edit

If, like alancalvitti you'd like the series to have exact rational representation, you can always use Fractions:

from fractions import Fraction

def rrange(start=0, stop=1, jump=0.1):
    nsteps = int((stop-start)/jump)
    return [Fraction(i, nsteps) for i in range(nsteps)]

[*] In particular, frange() returns a list, not a generator. But it sufficed for my needs.

Berglund answered 14/11, 2018 at 0:57 Comment(5)
If you want to include the stop value in the output, by adding stop+jump, this method then reverts to the naive result with bad floating points in the middle, try frange(0,1.1,0.1) and even more of those with a choice like frange(0,1.05,0.1)Behlau
@alancalvitti: What is your definition of a "bad" floating point? Yes, the results may not print nicely, but frange() provides the closest set of evenly spaced values within the limits of floating point representation. How would you improve it?Berglund
good point, I'm so used to high level language where you would range over rational numbers for such a task, that Py feels like assembly.Behlau
Assembly? Hrrumph! ;) Of course Python CAN provide exact representation with Fractions: docs.python.org/3/library/fractions.htmlBerglund
Right, thanks, but for example, the language I like automatically converts these types, so 1/2 is a rational, while 1/2.0 is float, there's no need to declare them as such - leave declarations to Java, which is even more lower/assembly than Py.Behlau
I
3

This can be done with numpy.arange(start, stop, stepsize)

import numpy as np

np.arange(0.5,5,1.5)
>> [0.5, 2.0, 3.5, 5.0]

# OBS you will sometimes see stuff like this happening, 
# so you need to decide whether that's not an issue for you, or how you are going to catch it.
>> [0.50000001, 2.0, 3.5, 5.0]

Note 1: From the discussion in the comment section here, "never use numpy.arange() (the numpy documentation itself recommends against it). Use numpy.linspace as recommended by wim, or one of the other suggestions in this answer"

Note 2: I have read the discussion in a few comments here, but after coming back to this question for the third time now, I feel this information should be placed in a more readable position.

Italianize answered 5/11, 2019 at 15:55 Comment(0)
G
2

Usage

# Counting up
drange(0, 0.4, 0.1)
[0, 0.1, 0.2, 0.30000000000000004, 0.4]

# Counting down
drange(0, -0.4, -0.1)
[0, -0.1, -0.2, -0.30000000000000004, -0.4]

To round each step to N decimal places

drange(0, 0.4, 0.1, round_decimal_places=4)
[0, 0.1, 0.2, 0.3, 0.4]

drange(0, -0.4, -0.1, round_decimal_places=4)
[0, -0.1, -0.2, -0.3, -0.4]

Code

def drange(start, end, increment, round_decimal_places=None):
    result = []
    if start < end:
        # Counting up, e.g. 0 to 0.4 in 0.1 increments.
        if increment < 0:
            raise Exception("Error: When counting up, increment must be positive.")
        while start <= end:
            result.append(start)
            start += increment
            if round_decimal_places is not None:
                start = round(start, round_decimal_places)
    else:
        # Counting down, e.g. 0 to -0.4 in -0.1 increments.
        if increment > 0:
            raise Exception("Error: When counting down, increment must be negative.")
        while start >= end:
            result.append(start)
            start += increment
            if round_decimal_places is not None:
                start = round(start, round_decimal_places)
    return result

Why choose this answer?

  • Many other answers will hang when asked to count down.
  • Many other answers will give incorrectly rounded results.
  • Other answers based on np.linspace are hit-and-miss, they may or may not work due to difficulty in choosing the correct number of divisions. np.linspace really struggles with decimal increments of 0.1, and the order of divisions in the formula to convert the increment into a number of splits can result in either correct or broken code.
  • Other answers based on np.arange are deprecated.

If in doubt, try the four tests cases above.

Gearhart answered 7/2, 2020 at 12:38 Comment(1)
What makes you think np.arange is deprecated?Arbiter
D
1

i wrote a function that returns a tuple of a range of double precision floating point numbers without any decimal places beyond the hundredths. it was simply a matter of parsing the range values like strings and splitting off the excess. I use it for displaying ranges to select from within a UI. I hope someone else finds it useful.

def drange(start,stop,step):
    double_value_range = []
    while start<stop:
        a = str(start)
        a.split('.')[1].split('0')[0]
        start = float(str(a))
        double_value_range.append(start)
        start = start+step
    double_value_range_tuple = tuple(double_value_range)
   #print double_value_range_tuple
    return double_value_range_tuple
Declivity answered 19/11, 2015 at 4:53 Comment(0)
M
1

Whereas integer-based ranges are well defined in that "what you see is what you get", there are things that are not readily seen in floats that cause troubles in getting what appears to be a well defined behavior in a desired range.

There are two approaches that one can take:

  1. split a given range into a certain number of segment: the linspace approach in which you accept the large number of decimal digits when you select a number of points that does not divide the span well (e.g. 0 to 1 in 7 steps will give a first step value of 0.14285714285714285)

  2. give the desired WYSIWIG step size that you already know should work and wish that it would work. Your hopes will often be dashed by getting values that miss the end point that you wanted to hit.

Multiples can be higher or lower than you expect:

>>> 3*.1 > .3  # 0.30000000000000004
True

>>> 3*.3 < 0.9  # 0.8999999999999999
True

You will try to avoid accumulating errors by adding multiples of your step and not incrementing, but the problem will always present itself and you just won't get what you expect if you did it by hand on paper -- with exact decimals. But you know it should be possible since Python shows you 0.1 instead of the underlying integer ratio having a close approximation to 0.1:

>>> (3*.1).as_integer_ratio()
(1351079888211149, 4503599627370496)

In the methods offered as answers, the use of Fraction here with the option to handle input as strings is best. I have a few suggestions to make it better:

  1. make it handle range-like defaults so you can start from 0 automatically
  2. make it handle decreasing ranges
  3. make the output look like you would expect if you were using exact arithmetic

I offer a routine that does these same sort of thing but which does not use the Fraction object. Instead, it uses round to create numbers having the same apparent digits as the numbers would have if you printed them with python, e.g. 1 decimal for something like 0.1 and 3 decimals for something like 0.004:

def frange(start, stop, step, n=None):
    """return a WYSIWYG series of float values that mimic range behavior
    by excluding the end point and not printing extraneous digits beyond
    the precision of the input numbers (controlled by n and automatically
    detected based on the string representation of the numbers passed).

    EXAMPLES
    ========

    non-WYSIWYS simple list-comprehension

    >>> [.11 + i*.1 for i in range(3)]
    [0.11, 0.21000000000000002, 0.31]

    WYSIWYG result for increasing sequence

    >>> list(frange(0.11, .33, .1))
    [0.11, 0.21, 0.31]

    and decreasing sequences

    >>> list(frange(.345, .1, -.1))
    [0.345, 0.245, 0.145]

    To hit the end point for a sequence that is divisibe by
    the step size, make the end point a little bigger by
    adding half the step size:

    >>> dx = .2
    >>> list(frange(0, 1 + dx/2, dx))
    [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]

    """
    if step == 0:
        raise ValueError('step must not be 0')
    # how many decimal places are showing?
    if n is None:
        n = max([0 if '.' not in str(i) else len(str(i).split('.')[1])
                for i in (start, stop, step)])
    if step*(stop - start) > 0:  # a non-null incr/decr range
        if step < 0:
            for i in frange(-start, -stop, -step, n):
                yield -i
        else:
            steps = round((stop - start)/step)
            while round(step*steps + start, n) < stop:
                steps += 1
            for i in range(steps):
                yield round(start + i*step, n)
Maggi answered 12/4, 2021 at 6:43 Comment(0)
H
1

I was having the same issue to plot the probabilities on y-axis. I have used this:

list(map(lambda x : x.round(1), np.linspace(0.0, 1.0, num=6)))

This produces:

[0.0, 0.2, 0.4, 0.6, 0.8, 1.0]

You can play with round() for the decimal part.

Hardboard answered 23/6, 2023 at 18:29 Comment(0)
P
0
def Range(*argSequence):
    if len(argSequence) == 3:
        imin = argSequence[0]; imax = argSequence[1]; di = argSequence[2]
        i = imin; iList = []
        while i <= imax:
            iList.append(i)
            i += di
        return iList
    if len(argSequence) == 2:
        return Range(argSequence[0], argSequence[1], 1)
    if len(argSequence) == 1:
        return Range(1, argSequence[0], 1)

Please note the first letter of Range is capital. This naming method is not encouraged for functions in Python. You can change Range to something like drange or frange if you want. The "Range" function behaves just as you want it to. You can check it's manual here [ http://reference.wolfram.com/language/ref/Range.html ].

Plasticine answered 12/1, 2017 at 9:17 Comment(0)
R
0

Is there a range() equivalent for floats in Python? NO Use this:

def f_range(start, end, step, coef=0.01):
    a = range(int(start/coef), int(end/coef), int(step/coef))
    var = []
    for item in a:
        var.append(item*coef)
    return var
Redan answered 12/1, 2017 at 10:0 Comment(4)
Pretty bad solution, try f_range(0.01,0.02,0.001)... For most practical purposes, arange from Numpy is a simple, safe and fast solution.Anticlockwise
You are right. With numpy is 1.8 faster than my code.Redan
You are right. With numpy is 1.8 faster than my code. But the system where I work is completely closed. Only Python and pyserial no more.Redan
Numpy not install on old PCRedan
L
0

I think that there is a very simple answer that really emulates all the features of range but for both float and integer. In this solution, you just suppose that your approximation by default is 1e-7 (or the one you choose) and you can change it when you call the function.

def drange(start,stop=None,jump=1,approx=7): # Approx to 1e-7 by default
  '''
  This function is equivalent to range but for both float and integer
  '''
  if not stop: # If there is no y value: range(x)
      stop= start
      start= 0
  valor= round(start,approx)
  while valor < stop:
      if valor==int(valor):
          yield int(round(valor,approx))
      else:
          yield float(round(valor,approx))
      valor += jump
  for i in drange(12):
      print(i)
Larena answered 14/10, 2017 at 12:51 Comment(0)
O
0

Talk about making a mountain out of a mole hill. If you relax the requirement to make a float analog of the range function, and just create a list of floats that is easy to use in a for loop, the coding is simple and robust.

def super_range(first_value, last_value, number_steps):
    if not isinstance(number_steps, int):
        raise TypeError("The value of 'number_steps' is not an integer.")
    if number_steps < 1:
        raise ValueError("Your 'number_steps' is less than 1.")

    step_size = (last_value-first_value)/(number_steps-1)

    output_list = []
    for i in range(number_steps):
        output_list.append(first_value + step_size*i)
    return output_list

first = 20.0
last = -50.0
steps = 5

print(super_range(first, last, steps))

The output will be

[20.0, 2.5, -15.0, -32.5, -50.0]

Note that the function super_range is not limited to floats. It can handle any data type for which the operators +, -, *, and / are defined, such as complex, Decimal, and numpy.array:

import cmath
first = complex(1,2)
last = complex(5,6)
steps = 5

print(super_range(first, last, steps))

from decimal import *
first = Decimal(20)
last = Decimal(-50)
steps = 5

print(super_range(first, last, steps))

import numpy as np
first = np.array([[1, 2],[3, 4]])
last = np.array([[5, 6],[7, 8]])
steps = 5

print(super_range(first, last, steps))

The output will be:

[(1+2j), (2+3j), (3+4j), (4+5j), (5+6j)]
[Decimal('20.0'), Decimal('2.5'), Decimal('-15.0'), Decimal('-32.5'), Decimal('-50.0')]
[array([[1., 2.],[3., 4.]]),
 array([[2., 3.],[4., 5.]]),
 array([[3., 4.],[5., 6.]]),
 array([[4., 5.],[6., 7.]]),
 array([[5., 6.],[7., 8.]])]
Ossetia answered 1/9, 2020 at 22:21 Comment(0)
V
0

Wow. Y'all are really overthinking this.

What are you doing? Do you really need a list of steps in a range, or are you just checking if a float is in the range of two other floats?

Parsing video, I often need to check if a splice_point is in a certain PTS range. I just do

    if start < splice_point < stop:

if you need to check with a step:

  def in_range_with_step(splice_point,start,stop,step):
     if start < splice_point < stop:
         if (splice_point -start) % step ==0:
             return True
     return False

if you just want the steps in the range:

    def a_range(start,stop,step):
        the_range=[]
        while start < stop:  
            the_range.append(start)
            start+=step
       return the_range   
Ventral answered 10/2 at 12:29 Comment(0)
C
-1

There will be of course some rounding errors, so this is not perfect, but this is what I use generally for applications, which don't require high precision. If you wanted to make this more accurate, you could add an extra argument to specify how to handle rounding errors. Perhaps passing a rounding function might make this extensible and allow the programmer to specify how to handle rounding errors.

arange = lambda start, stop, step: [i + step * i for i in range(int((stop - start) / step))]

If I write:

arange(0, 1, 0.1)

It will output:

[0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9]
Claviform answered 30/3, 2020 at 2:37 Comment(0)
C
-2

There several answers here that don't handle simple edge cases like negative step, wrong start, stop etc. Here's the version that handles many of these cases correctly giving same behaviour as native range():

def frange(start, stop=None, step=1):
  if stop is None:
    start, stop = 0, start
  steps = int((stop-start)/step)
  for i in range(steps):
    yield start
    start += step  

Note that this would error out step=0 just like native range. One difference is that native range returns object that is indexable and reversible while above doesn't.

You can play with this code and test cases here.

Crenshaw answered 2/3, 2019 at 0:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.