An equivalent of numpy.linspace
, but as a pure-Python lazy sequence.
Like NumPy's linspace
, but unlike the spread
and frange
recipes listed here, the num
argument specifies the number of values, not the number of intervals, and the range is closed, not half-open.
Although this is primarily designed for floats, it will work for Fraction
, Decimal
, NumPy arrays (although this would be silly) and even datetime
values.
This recipe can also serve as an example for creating lazy sequences.
See the discussion below for caveats.
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 | class linspace(collections.abc.Sequence):
"""linspace(start, stop, num) -> linspace object
Return a virtual sequence of num numbers from start to stop (inclusive).
If you need a half-open range, use linspace(start, stop, num+1)[:-1].
"""
def __init__(self, start, stop, num):
if not isinstance(num, numbers.Integral) or num <= 1:
raise ValueError('num must be an integer > 1')
self.start, self.stop, self.num = start, stop, num
self.step = (stop-start)/(num-1)
def __len__(self):
return self.num
def __getitem__(self, i):
if isinstance(i, slice):
return [self[x] for x in range(*i.indices(len(self)))]
if i < 0:
i = self.num + i
if i >= self.num:
raise IndexError('linspace object index out of range')
if i == self.num-1:
return self.stop
return self.start + i*self.step
def __repr__(self):
return '{}({}, {}, {})'.format(type(self).__name__,
self.start, self.stop, self.num)
def __eq__(self, other):
if not isinstance(other, linspace):
return False
return ((self.start, self.stop, self.num) ==
(other.start, other.stop, other.num))
def __ne__(self, other):
return not self==other
def __hash__(self):
return hash((type(self), self.start, self.stop, self.num))
|
For Python 3.3+ code, use collections.abc.Sequence
instead of collections.Sequence
.
There are two obvious simple algorithms for linspace
(plus some more advanced ones):
- division first:
start + i*(stop-start)/(num-1)
- multiplication first:
(stop*i + start*(num-i-1))/(num-1)
This recipe uses the former, primarily because it's the one used by NumPy.
Both are simple and fast; neither accumulates errors (both will close to the minimum possible number of 1 ulp errors distributed evenly throughout the range, which is as good as you can hope for with floats); but neither is perfect:
- Division first underflows denormal numbers to
0
. (See NumPy bug #5437) - Multiplication first overflows very large numbers to
inf
. - Multiplication first doesn't match NumPy's results.
- Division first errors show up worse in a few highly visible cases (e.g.,
linspace(0, 1, 11)[3] == 0.30000000000000004
). - Multiplication first requires types that can be multiplied and divided by integers, so it will not work with, e.g.,
datetime
. (Note that division first only multiplies and divides _differences_ between values—so, withdatetime
,timedelta
s.)
For many lazy sequences, a slice should return an instance of the same sequence. This is how the builtin range
works, for instance. However, floating point rounding makes that impossible for linspace
; a slice could at best guarantee a sequence whose values are within 2 ulp of the original values. So, a slice instead returns a list.
Inheriting from Sequence
means that linspace
provides __contains__
, index
, and count
methods, using the default (linear-search) implementation. It's generally a bad idea to use these (for the same reason it's a bad idea to compare floats with ==
), but not providing them would mean linspace
is no longer a Sequence
. Of course an O(1)
implementation could be provided pretty easily, but that would just encourage (mis)use.
Thanks for linking to my recipes, but you linked to the same one twice :-(
I think you meant spread. (Hope I got the markdown syntax right...)
Ah drat. Let's try that link again: spread