Your suspicion is correct: the iterator has been consumed.
In actuality, your iterator is a generator, which is an object which has the ability to be iterated through only once.
type((i for i in range(5))) # says it's type generator
def another_generator():
yield 1 # the yield expression makes it a generator, not a function
type(another_generator()) # also a generator
The reason they are efficient has nothing to do with telling you what is next “by reference.” They are efficient because they only generate the next item upon request; all of the items are not generated at once. In fact, you can have an infinite generator:
def my_gen():
while True:
yield 1 # again: yield means it is a generator, not a function
for _ in my_gen(): print(_) # hit ctl+c to stop this infinite loop!
Some other corrections to help improve your understanding:
- The generator is not a pointer, and does not behave like a pointer as you might be familiar with in other languages.
- One of the differences from other languages: as said above, each result of the generator is generated on the fly. The next result is not produced until it is requested.
- The keyword combination
forinaccepts an iterable object as its second argument. - The iterable object can be a generator, as in your example case, but it can also be any other iterable object, such as a
list, ordict, or astrobject (string), or a user-defined type that provides the required functionality. - The
iterfunction is applied to the object to get an iterator (by the way: don’t useiteras a variable name in Python, as you have done – it is one of the keywords). Actually, to be more precise, the object’s__iter__method is called (which is, for the most part, all theiterfunction does anyway;__iter__is one of Python’s so-called “magic methods”). - If the call to
__iter__is successful, the functionnext()is applied to the iterable object over and over again, in a loop, and the first variable supplied toforinis assigned to the result of thenext()function. (Remember: the iterable object could be a generator, or a container object’s iterator, or any other iterable object.) Actually, to be more precise: it calls the iterator object’s__next__method, which is another “magic method”. - The
forloop ends whennext()raises theStopIterationexception (which usually happens when the iterable does not have another object to yield whennext()is called).
You can “manually” implement a for loop in python this way (probably not perfect, but close enough):
try:
temp = iterable.__iter__()
except AttributeError():
raise TypeError("'{}' object is not iterable".format(type(iterable).__name__))
else:
while True:
try:
_ = temp.__next__()
except StopIteration:
break
except AttributeError:
raise TypeError("iter() returned non-iterator of type '{}'".format(type(temp).__name__))
# this is the "body" of the for loop
continue
There is pretty much no difference between the above and your example code.
Actually, the more interesting part of a for loop is not the for, but the in. Using in by itself produces a different effect than for in, but it is very useful to understand what in does with its arguments, since for in implements very similar behavior.
-
When used by itself, the
inkeyword first calls the object’s__contains__method, which is yet another “magic method” (note that this step is skipped when usingforin). Usinginby itself on a container, you can do things like this:1 in [1, 2, 3] # True 'He' in 'Hello' # True 3 in range(10) # True 'eH' in 'Hello'[::-1] # True -
If the iterable object is NOT a container (i.e. it doesn’t have a
__contains__method),innext tries to call the object’s__iter__method. As was said previously: the__iter__method returns what is known in Python as an iterator. Basically, an iterator is an object that you can use the built-in generic functionnext()on1. A generator is just one type of iterator. - If the call to
__iter__is successful, theinkeyword applies the functionnext()to the iterable object over and over again. (Remember: the iterable object could be a generator, or a container object’s iterator, or any other iterable object.) Actually, to be more precise: it calls the iterator object’s__next__method). - If the object doesn’t have a
__iter__method to return an iterator,inthen falls back on the old-style iteration protocol using the object’s__getitem__method2. - If all of the above attempts fail, you’ll get a
TypeErrorexception.
If you wish to create your own object type to iterate over (i.e, you can use for in, or just in, on it), it’s useful to know about the yield keyword, which is used in generators (as mentioned above).
class MyIterable():
def __iter__(self):
yield 1
m = MyIterable()
for _ in m: print(_) # 1
1 in m # True
The presence of yield turns a function or method into a generator instead of a regular function/method. You don’t need the __next__ method if you use a generator (it brings __next__ along with it automatically).
If you wish to create your own container object type (i.e, you can use in on it by itself, but NOT for in), you just need the __contains__ method.
class MyUselessContainer():
def __contains__(self, obj):
return True
m = MyUselessContainer()
1 in m # True
'Foo' in m # True
TypeError in m # True
None in m # True
1 Note that, to be an iterator, an object must implement the iterator protocol. This only means that both the __next__ and __iter__ methods must be correctly implemented (generators come with this functionality “for free”, so you don’t need to worry about it when using them). Also note that the ___next__ method is actually next (no underscores) in Python 2.
2 See this answer for the different ways to create iterable classes.