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))
Answer from Grzegorz Rożniecki on Stack OverflowYou 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))
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.]
How to step by floats in a range function?
Python Float point range() operator implementation - Code Review Stack Exchange
Float contained in range - Ideas - Discussions on Python.org
float numbers with range () function
Videos
The range function does not handle floats
I am trying to convert something from PHP to Python, but came across a problem since the range function can only step by integers.
I will divide a circle into 16 parts. In PHP I have written:
for ($degrees = 0; $degrees < 360; $degrees += 22.5) {
...I was going to write it like this in Python:
for degrees in range(0, 360, 22.5):
...What is the easiest / nicest way to achieve this?
Let's start with comments about the code you've submitted, before we discuss some more important overlying concepts and design decisions.
Good
- Docstrings for each method (some could be more helpful though, like
__len__) - Some comments for perhaps unclear lines
- Python 3 style classes (didn't inherit from
object) - You have unit tests!
- Good use of
ValueError
Improvements
- You don't need
# -*- coding: utf-8 -*-with Python 3 - You formatting is pretty inconsistent. Try PEP8 (it's the standard formatting that most projects adhere to)
- You seem to prefix a lot of variables with
_. It seems like you may be confused about kwargs. If you dofoo(bar=1),baris not a variable. So if you hadbar = 1, it's perfectly legal (and encouraged) to dofoo(bar=bar). Although, consider if it really is unclear what the param means. Perhaps a positional arg works just fine. If that's not the case, we basically exclusively use_for private instance properties (likeself._start) - Your
test_compare_basicisn't actually a test case. Methods starting withtest_should exercise a specific test case or group of test cases.test_compare_basicis actually a generic way of testing any range. Writing it was a fantastic idea, because it makes writing the later tests much more succinct and clear. However, naming ittest_means that it is run by the test harness (and it shouldn't be run alone). I usually call these functionsassert*to match the unittest framework (camelCase unfortunately, as this is whatunittestdoes, but you could break this if you wanted). Eg. name itassertFloatRangeCorrect. Then your tests look like:
def test_simple_float_ranges(self):
# These reads much more like sentences now...
self.assertFloatRangeCorrect(0.5, 5.0, 0.5)
self.assertFloatRangeCorrect(1, 2, 0.25)
- I see you have
try/exceptin your tests to print a message. You shouldn't be doing this. For one, the message won't be grouped with the error (or it's stack trace). You can just pass the extra optionalmsgargument toassertEqual:self.assertEqual(len(actual), len(expected), f'len({actual}) != len({expected})')(notice my use of f-strings, they're definitely cleaner here). By doing this, your tests become a lot shorter and you avoid thetry/except/raisedance. - For testing exact equality, instead of
ziping two iterables just useself.assertEqual(iterable_a, iterable_b). This will also produce a nice error message automatically. - Your check against
Realis strange (more on this later) - What is going on with the
_precision,_start, and_step? You shouldn't have those. - Don't use
*argslike this in__init__. Use arg defaults. Eg.def __init__(self, start=0, stop=1, step=1)(I know this doesn't work perfectly with your current argument scheme, but later I'll argue you should change it) - In tuple unpacking (
(self.stop, ) = args) you don't need the parens' - Your
__iter__docstring should be a comment. It doesn't explain to a user ofFloatRangehow to use the class. But you can eliminate it, because that's obvious from the fact youreturn self. - Having the range be it's own iterator is strange (and uncommon). And I'll argue against it later.
- Minor nit but in
__str__userepr(self). We usually don't call dunder methods (with the prominent exception beingsuper().__init__(...)). - In
__repr__use f-strings. They much easier to construct and they give a better idea of the output format. - You should put
FloatRangeinfloat_range.pyinstead ofFloatRange.py - Comparing floats with
0is usually not what you want. Rarely will the result of arithmetic be exactly 0. You wantmath.isclose
Now, let's talk about the big concept here. Python's builtin range doesn't support floats as you likely know. There is good reason for this. Floating point math does not always work as pen and paper decimal math due to representation issues. A similar problem would be adding 1/3 as a decimal by hand 3 times. You expect 1, but since you only have a finite number of decimals, it won't be exactly 1 (it'll be 0.99...).
What does this have to do with your float range? It poses two interesting problems for the user of FloatRange if they're used to range().
The upper bound may not produce the range that you expect due to representation errors alluded to above. Where we can know that range(5) will always have 5 numbers, we can't really be so sure about the length of range(0, 10, 0.1) (that is, unless the start, stop, and step are exactly 0, 10, and 0,1--floats are deterministic given the same operations in the same order) because of floating point inaccuracies. Sure, we can divide like you did. However, with your precision factor, I suspect that length won't always be right. The trouble here is we need to decide what stop means. For range, it's much easier to say because integers are exact. range can be thought of as filling in the number line between start and stop (excluding stop). We probably want to exclude stop too for consistency, but FloatRange is more of a finite set of points between start and stop exclusive. Because of this, membership is a little more tricky. You could define membership as being within the range or being an explicit member from the iteration of the range. For range(), these two are equivalent because integers are countable.
You seem to have chosen the later definition of __contains__. But, it does beg the question: is this actually meaningful? Is there a context where you'd need to check floating point within a tolerance of some number of (finite-representable) discrete points in some range.
As a result of these issues, this FloatRange is way more complicated than it needs to be. You also make some common mistakes with comparing floating point numbers that will fail for extrema.
As an aside, let's also take a look at your constructor parameters. You allow for 1, 2, or 3 arguments (excluding precision), like range. I think the only really meaningful constructors are the 2 and 3 argument ones. The single argument assumes a start of 0 and step of 1. But, then this is precisely just range (so why not use range?). It seems like it's really only meaningful to define a floating point stop and step (with a start of 0) or all 3. But, if you really feel strongly about the 1 argument case, you can of course keep it.
Now that we've discussed the problems, let's take a stab at some solutions. I see two solutions.
You want a range starting at start that adds step until the number is >= stop. This is more like what you've implemented (and similar to range in some regards, except its length is not constant-time computable). I'd recommend not defining __len__. If you do, you may want to warn that it is not constant time. Why is this? Well you could do (stop - start) / step, but as you likely found, this has accuracy issues. These are the same representation issues we mentioned above. Furthermore, it is difficult to account for fancier bounds checking (ie. if you want to keep producing numbers until one is less than or "close to" stop for some definition of close to--like within some threshold).
from itertools import count, takewhile
class FloatRange:
def __init__(self, start, stop=None, step=1):
# No to handle # of arguments manually
if stop is None:
stop = start
start = 0
if any(not isinstance(x, float) for x in (start, stop, step)):
raise ValueError('start, stop, step must be floats')
if (start < stop and step < 0) or (start > stop and step > 0):
raise ValueError('step sign must match (stop - start)')
self.start = start
self.stop = stop
self.step = step
def __iter__(self):
return takewhile(lambda x: x < self.stop, (self.start + i * self.step
for i in count(0)))
Note we need no custom iterator or lots of logic. itertools can do most of the heavy lifting. Furthermore, we can update the predicate lambda x: to also include some definition of less than or close to like so: lambda x: x < self.stop and not math.isclose(x, self.stop, ...). Look at math.isclose to see what you need to pass (you need two params, not just tolerance). If you really need __len__:
def __len__(self):
count = 0
for x in self:
count += 1
return count
I'd recommend against __contains__ here because determining the index count have precision issues for extrema. Eg. self.step * round((x - self.start) / self.step) could be unstable.
You want a range that takes some pre-determinted number of steps of size step from start. Notice there is no stop here. __len__ is immediately obvious. I'd recommend maybe not defining __contains__ for now.
This case is very straightfoward:
class FloatRange:
def __init__(self, start, *, step=1, steps=0): # here I require step and steps to be kwargs for clarity
if any(not isinstance(x, float) for x in (start, step)):
raise ValueError('start and step must be floats')
if not isinstance(steps, int) or x < 0:
raise ValueError('steps must be a positive integer')
self.start = start
self.step = step
self.steps = steps
def __iter__(self):
return (self[i] for i in range(self.steps))
def __getitem__(self, i):
if not 0 <= i < self.steps:
raise IndexError('FloatRange index out of range')
return self.start + i * self.step
def __len__(self):
return self.steps
Here we can easily define __len__. __contains__ is still tricky, because determining the index of a potential member of the range could be unstable. Here, though, because we can compute the end of the range in constant time (it's exactly start + steps * step), we can do some sort of clever binary search. More specifically, we can search for numbers close to the desired numbers (for some metric of closeness that you determine) and stop once the numbers we find are less than the desired number and decreasing (negative step) OR greater than the desired number and increasing (positive step). This comes nearly for free because we were able to define __getitem__ (which we couldn't before because we couldn't bound the indices). We note that in this way, this FloatRange behaves much more like range() even though the constructor parameters are different.
You may argue that since steps must be an integer, if you placed some sane limits on it then it would be impossible to construct a member whose index calculation is unstable. Unfortunately, because the index calculation involves a multiply/divide this is just not the case. By reading the IEEE 754 spec you can construct a degenerate case. Specifically, for large indices (which would initially be a float when resulting from the index computation) the floating point resolution is so wide that converting to an int does not produce the correct index. This is especially true for Python because int is arbitrary precision.
You probably should replace **kwargs in __init__ with precision=10**10:
def __init__(self, *args, precision=10**10):
self.precision = precision
self.set_precision(precision) # not self._precision
and remove last huge block of __init__ that validates kwargs
The way you implement __iter__ has following disadvantage:
r = FloatRange(1., 2., 0.3)
iterator1 = iter(r)
iterator2 = iter(r)
assert next(iterator1) == 1.0
assert next(iterator2) == 1.3
iterator3 = iter(r)
assert next(iterator3) == 1.6
If you run the same code with built-in python range iterator2 and iterator3 will produce original sequence, not empty. You probably should remove __next__ method and return generator in __iter__:
def __iter__(self):
output = self.start
while (self.step > 0) == (output < self.stop) and output != self.stop:
yield output
output += self.step
Hi, I have a section of code which takes a score from the user, typecasts it into a float value and saves it under the variable name "score". Then I have a line of code which reads:
while score not in range(0, 101):
However, when I input a float, I get an invalid input error message from python. I think it's because I can't use the range() function with floats. Is there a way around this? If anyone knows how to solve this, then your help would be greatly appreciated. Thank you in advance.