Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 37 lines
 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, with datetime, timedeltas.)

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.

2 comments

Steven D'Aprano 9 years, 1 month ago  # | flag

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...)

Steven D'Aprano 9 years, 1 month ago  # | flag

Ah drat. Let's try that link again: spread