A few weeks ago I had to implement a few AI algorithms for a college project. I ended up making some code that might be useful to other people, so I decided to share it.
A range of floats
One of the subtasks was to generate all of the numbers in a given interval:
- [0, 5) with a step of 1 should result in: [0, 1, 2, 3, 4]
- [0, 0.5) with a step of 0.1 should result in: [0, 0.1, 0.3, 0.4]
Most people familiar with python would say we could simply use
range
.
This very useful function is usually what I'd go with but when I tried it I got this:
>>> list(range(0, 5, 1))
[0, 1, 2, 3, 4]
>>>list(range(0, 0.5, 0.1))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'float' object cannot be interpreted as an integer
range
clearly solves the first case but, why not the second? I thought I was
doing something wrong like changing the argument order or something. So I went to
look for help in the docs.
I got a little disapointed when I found out it doesn't support floats. As I really
needed it I decided to make my own float_range
.
My first attempt was:
def float_range(start, stop=None, step=1):
if stop is None:
stop, start = start, 0
while(start < stop):
yield start
start += step
I felt quite smart with this solution unil I found an issue which I believe to be the reason for floats to not being supported by range.
>>> list(float_range(0, 5, 1))
[0, 1, 2, 3, 4]
>>> list(float_range(5))
[0, 1, 2, 3, 4]
>>> list(float_range(0, 0.5, 0.1))
[0, 0.1, 0.2, 0.30000000000000004, 0.4]
Things were good to be true. This problem of floating point arithmetics is well known and it's related to the difficulty of representing "broken" numbers in binary. For more information take a look here.
There's a builtin function in python that helped me solving this problem.
round
,
as the name suggests, rounds the number to the closest integer. Despite this
feature being quite amazing it's not the best part of round
. There's a second
optional parameter to specify how many digits to be taken into consideration when
rounding.
Having found this out I tried it a second time:
def float_range(start, stop=None, step=1):
if stop is None:
stop, start = start, 0
while(start < stop):
yield round(start, 1)
start += step
This version is much better but still fails when we need dynamic precision.
>>> list(float_range(0, 0.5, 0.1))
[0, 0.1, 0.2, 0.3, 0.4]
>>> list(float_range(0, 0.05, 0.01))
[0, 0.0, 0.0, 0.0, 0.0]
This problem would repeat itself as long as I kept the precision constant. I then decided the best solution would be to determine the step precision at runtime. I went ahead and wrote a helper function to do so.
def precision(number):
try:
res = len(str(number).split('.')[1])
except IndexError:
res = 1
return res
We transform the number into a string, split it in .
and the precision will be
the length of the second string. If splitting the string doesn't result in two
substrings, we're dealing with an integer and the precision is 1.
With this helper function we can enhance the earlier solution:
def float_range(start, stop=None, step=1):
if stop is None:
stop, start = start, 0
while(start < stop):
yield round(start, precision(step))
start += step
This last version finally works as expected.
>>> list(float_range(0, 5, 1))
[0, 1, 2, 3, 4]
>>> list(float_range(0, 0.5, 0.1))
[0, 0.1, 0.2, 0.3, 0.4]
>>> list(float_range(0, 0.5, 0.01))
[0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29, 0.3, 0.31, 0.32, 0.33, 0.34, 0.35, 0.36, 0.37, 0.38, 0.39, 0.4, 0.41, 0.42, 0.43, 0.44, 0.45, 0.46, 0.47, 0.48, 0.49]