The most commonly used functions in Python 3 that return iterators (which is what we are discussing here) are: zip(), enumerate(), reversed(), map(), filter(). It seems to be the trend to return iterators, as some of those returned lists in Python 2; be on the lookout for more in future versions. I don’t know of any way to recognize which functions return iterators, except of course, by reading the docs or other instructions on their use.
Technically, range() is not an iterator. range can be “examined” in ways that iterators cannot:
my_range = range(5)
print(len(my_range))
print(my_range[3])
print(3 in my_range)
print(list(my_range))
Output:
5
3
True
[0, 1, 2, 3, 4]
Now let’s try it with a true iterator, say, the output of the enumerate() function:
my_list = ['a','b','c']
my_enumerate = enumerate(my_list)
#print(len(my_enumerate)) # raises error
#print(my_enumerate[2]) # raises error
print((1, 'b') in my_enumerate)
print(list(my_enumerate))
Output:
True # yes, (1, 'b') is in my_enumerate ...
[(2, 'c')] # ... but discovering this partially exhausted the iterator!
I’m not sure - I’ve never seen an answer to this. Presumably, the iter and next functions that define iterators could be rewritten in such a way as to permit reuse.
My own theory of the reason is that we would like functions to return the same output from a given input, but an iterator can be used to change the iterable which generated it.
my_list = ['a','b','c']
my_enumerate = enumerate(my_list)
for idx, element in my_enumerate:
my_list[idx] = element + 'x'
print(my_list)
Output:
[‘ax’, ‘bx’, ‘cx’]
So, at this point, what should my_enumerate look like if it could be re-used???
There is a lot about this on the internet. There is one thread on Stack Overflow that maintains that it is only the iterator type called “generator” that is exhaustible. That, at least, is not true.
Here are some good references.
The Iterator Protocol: How “For Loops” Work in Python
Iterables, iterators and generators, oh my! Part 1
Python: range is not an iterator!