Generators
- sequence creation object, able to iterate through potentially huge sequences without creating and storeing the entire sequence in memory.
- Every time you iterate through a generator, it keeps track of where it was the last time it was called and returns the next value. This is different from a normal function, which has no memory of previous calls and always starts at its first line with the same state.
Generator Functions
- create a potentially large sequence
- it returns ist value with a yield statmeent rather than return
def my_range(first=0, last=10, step=1):
number = first
while number < last:
yield number
number += step
=> NOTE: I need to find a good example.
- A generator can be run only once. Lists, sets, strings, and dictionaries exist in memory, but a generator creates its values on the fly and hands them out one at a time through an iterator. It doesn’t remember them, so you can’t restart or back up a generator.
Generator Comprehensions
- shorthand version of a generator function
genobj = (pair for pair in zip(['a','b'],['1','2']))
genobj
<genrator object <genexpr> at 0x10308fde0>
for thing in genobj:
print(thing)
('a','1')
('b','2')
06 Generators
A generator is a special class of function that serves as an iterator instead of returning a single value the generator returns a stream of values.
#generator.py
def main():
for i in range(25):
print(i, end = ' ')
print()
def inclusive_range(*args):
numargs = len(args)
start = 0
step = 1
# initialize parameters
if numargs < 1:
raise TypeError(f'at least 1 argument, got {numargs}')
elif numargs == 1:
stop = args[0]
elif numargs == 2:
(start, stop) = args
elif numargs == 3:
(start, stop, step) = args
else: raise TypeError(f'at most 3 arguments, got {numargs}')
# generator
i = start
while i <= stop:
yield i
i += step
if __name__ == '__main__': main()
If instead of calling my function I call range here and save and run you notice I’ve specified the number 25 and in a result it counts from zero to 24, which is 25 values starting at zero. I find that a little bit confusing myself at times and so I created a version of range that I call inclusive range. It simply works exactly like range only it returns all of the values from zero all the way up to 25, when I run this you see that my result now includes the 25. it’s great as an illustration of how a generator works, because that’s what range is is it’s a generator.
def main():
for i in inclusive_range(25):
print(i, end = ' ')
print()
def inclusive_range(*args):
numargs = len(args)
start = 0
step = 1
# initialize parameters
if numargs < 1:
raise TypeError(f'at least 1 argument, got {numargs}')
elif numargs == 1:
stop = args[0]
elif numargs == 2:
(start, stop) = args
elif numargs == 3:
(start, stop, step) = args
else: raise TypeError(f'at most 3 arguments, got {numargs}')
# generator
i = start
while i <= stop:
yield i
i += step
if __name__ == '__main__': main()
So let’s take a look at the generator function. First off you’ll notice that we use a variable argument list and our number of args is the length and we initialize a couple of variables and then we use this chain of elifs, if and elifs, to decide how to initialize our variables however many arguments we get.
You remember the range works if it’s just one argument that argument is the stop. If it has two arguments the two arguments are start and stop, and if it has three arguments the arguments are start, stop, and step, and if I have less than one argument I raise an error and if I have more than three arguments I raise an error.
Then the actual generator is down here at the bottom. We use a while loop, we initialize i at the beginning and while i <= stop then we have this yield. Now yield is like return except it’s for a generator. It yields a value and then after it yields the value the function continues until it yields the next value. So when we run this we get => it works exactly like range.
I can tell it to start at five instead of starting at zero and when I run this
def main():
for i in inclusive_range(5, 25):
print(i, end = ' ')
print()
def inclusive_range(*args):
numargs = len(args)
start = 0
step = 1
# initialize parameters
if numargs < 1:
raise TypeError(f'at least 1 argument, got {numargs}')
elif numargs == 1:
stop = args[0]
elif numargs == 2:
(start, stop) = args
elif numargs == 3:
(start, stop, step) = args
else: raise TypeError(f'at most 3 arguments, got {numargs}')
# generator
i = start
while i <= stop:
yield i
i += step
if __name__ == '__main__': main()
you notice it starts at five. And I can tell it to step by five if I like and when I run this you notice it counts by five. Five, 10, 15, 20, and 25.
def main():
for i in inclusive_range(5, 25, 5):
print(i, end = ' ')
print()
def inclusive_range(*args):
numargs = len(args)
start = 0
step = 1
# initialize parameters
if numargs < 1:
raise TypeError(f'at least 1 argument, got {numargs}')
elif numargs == 1:
stop = args[0]
elif numargs == 2:
(start, stop) = args
elif numargs == 3:
(start, stop, step) = args
else: raise TypeError(f'at most 3 arguments, got {numargs}')
# generator
i = start
while i <= stop:
yield i
i += step
if __name__ == '__main__': main()
It’s really very simple, most of the processing of this function is dealing with the arguments. The generator itself is really just this four lines of code.
You notice also that I’m using exceptions for my errors and we’ll talk about exceptions in a later chapter, but if I give it no arguments I get an error that says type error expected at least one argument, it got zero, and if I give it too many arguments it says type error expected at most three arguments got four and those are these exceptions right here, raise type error, raise type error. If I put this back it works as expected.
So a generator is a special case of a function that is useful for creating a series of values.