Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Python's range is a lazy sequence PREMIUM

Series: Looping
Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
4 min. read 3 min. video Python 3.9—3.13
Python Morsels
Watch as video
02:55

Python's range objects are lazy sequences.

range objects don't store their values

If you make a very large range, Python will make the range object very quickly.

>>> numbers = range(10**12)
>>> numbers
range(0, 1000000000000)

Unlike a list or a tuple, range objects don't actually store the values that they contain. They only compute those values just before we need them.

range answers questions lazily

So if we ask this range object for its last number, it computes that answer very quickly:

>>> numbers[-1]
999999999999

It also computes its length when we ask it for its length:

>>> len(numbers)

And if we were to loop over it, it computes each item as we loop:

>>> for n in numbers:
...     print(n)
...     if n > 3:
...         break
...
0
1
2
3
4

Are range objects iterators?

If you're familiar with iterables and iterators in Python, you might think that range objects seem like iterators.

And range objects do seem like iterators, but they're not iterators.

An iterator will compute its next item as you loop over it:

>>> colors = ["purple", "blue", "green", "orange"]
>>> color_iterator = iter(colors)
>>> for color in color_iterator:
...     print(color)
...
purple
blue
green
orange

And so will range objects:

>>> numbers = range(5)
>>> for n in numbers:
...     print(n)
...
0
1
2
3
4

But an iterator's items are consumed as you loop over it, so this iterator is actually empty now:

>>> for color in color_iterator:
...     print(color)
...

But unlike that iterator, our range object isn't empty:

>>> for n in numbers:
...     print(n)
...
0
1
2
3
4

That range object hasn't changed. It works exactly the same way as it did the first time we looped over it.

range objects don't get consumed

I often refer to iterators as lazy iterables.

While range objects are lazy, they're actually more sophisticated than iterators. You can think of them as lazy sequences.

If we ask a range object for an iterator (in order to loop over it manually) it will give us a new iterator object back:

>>> numbers = range(5)
>>> i1 = iter(numbers)
>>> next(i1)
0

If we ask it for another iterator object, it gives us a second iterator object that's independent of the first iterator:

>>> i2 = iter(numbers)
>>> next(i2)
0
>>> next(i2)
1

If we did the same thing with an iterator, we would get back the original iterator object:

>>> colors = ["purple", "blue", "green", "orange"]
>>> color_iterator = iter(colors)
>>> iterator_again = iter(color_iterator)
>>> color_iterator
<list_iterator object at 0x7f9f6ef70be0>
>>> iterator_again
<list_iterator object at 0x7f9f6ef70be0>

Unlike an iterator, a range object doesn't change its state. Instead, range objects generate a new iterator for us every time we loop over them.

Does any of this matter?

Now, you might ask, does it matter that range objects aren't iterators?

It doesn't matter if we're just trying to convey that they're lazy, but it would be less confusing to just refer to them as "lazy".

If you told me that something was an iterator, I can make some assumptions about that object.

If we start to loop over an iterator, and then we stop, that iterator will be partially consumed:

>>> colors = ["purple", "blue", "green", "orange"]
>>> color_iterator = iter(colors)
>>> for color in color_iterator:
...     print(color)
...     break
...
purple

So when we start looping over it again, it'll start up where it left off:

>>> for color in color_iterator:
...     print(color)
...
blue
green
orange

That's true of any iterator. But it's not true of range objects; range objects do the same thing every time we loop over them.

Also, iterators all work with the built-in next function to get their next item:

>>> colors = ["purple", "blue", "green", "orange"]
>>> color_iterator = iter(colors)
>>> next(color_iterator)
'purple'

But range objects don't, because range objects are not iterators:

>>> numbers = range(10)
>>> next(numbers)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'range' object is not an iterator

Python's range objects are lazy sequences

Python's range objects are not iterators.

Even though range objects are not iterators, they are lazy, meaning they don't actually store their data, they compute their data as you loop over them and as you index them. You can think of range objects as lazy sequences.

Series: Looping

Unlike, JavaScript, C, Java, and many other programming languages we don't have traditional C-style for loops. Our for loops in Python don't have indexes.

This small distinction makes for some big differences in the way we loop in Python.

To track your progress on this Python Morsels topic trail, sign in or sign up.

0%
Python Morsels
Watch as video
02:55
This is a free preview of a premium screencast. You have 2 previews remaining.