Generators in Python
Arfat Salman
Software Engineer at Pesto Tech
@salman_arfat
Presented at PyDelhi, Gurgaon
Goals
- Take a look at Python Generator Functions
- It's a powerful feature that is often not utilised to its fullest
- See how many constructs in python use generators
- See a few example implementations
- Have fun!
Disclaimer
- This is not an exhaustive tutorial. We are not going to learn every possible use cases.
- Instead, we are going to develop an intuitive understanding of where generators are useful and see some examples.
- Consult Python Docs and books such as Learning Python (5E) by Mark Lutz.
Iterables and Iterators
- An iterable is an object that has an __iter__ method which returns an iterator.
- Iterators are objects with __next__ defines. They
- return the next value in the iteration
- remember the state during iteration.
- update the state to point at the next value
- signal when it is done by raising StopIteration
- Iterables can also defines a __getitem__ method that can take sequential indexes starting from zero (and raises an IndexError when the indexes are no longer valid).
class Range(object):
def __init__(self, low, high):
self.start = low
self.end = high
def __iter__(self):
class Iterator(object):
def __init__(self, low, high):
self.start = low
self.end = high
self.current = low
def __next__(self):
if self.current >= self.end:
raise StopIteration
val = self.current
self.current += 1
return val
return Iterator(self.start, self.end)
my_range = Range(2, 10)
range_iterator = iter(my_range)
print('Using iterator and next - ')
print(next(range_iterator)) # 2
print(next(range_iterator)) # 3
print(next(range_iterator)) # 4
print('Using iterable - ')
for val in my_range:
print(val, end=' ')
# 2 3 4 5 6 7 8 9
Iteration Protocol
class Range(object):
def __init__(self, low, high):
self.start = low
self.end = high
self.current = low
def __iter__(self):
return self
def __next__(self):
if self.current >= self.end:
raise StopIteration
val = self.current
self.current += 1
return val
my_range = Range(2,10)
range_iterator = iter(my_range)
print('Using iterator and next - ')
print(next(range_iterator)) # 2
print(next(range_iterator)) # 3
print(next(range_iterator)) # 4
print('Using iterable - ')
for val in my_range:
print(val, end=' ')
# 5 6 7 8 9
Iteration
- Iteration is the process of applying iterators and getting sequential values.
- Python's for loop uses iterators internally, if available.
- An example -
for elm in [1, 2, 'a', 'b']:
print(elm)
# Results
1
2
a
b
Iteration in Python
- Iterating over a Dict
details = {
'first_name': 'Arfat',
'last_name': 'Salman',
'twitter_handle': '@salman_arfat'
}
for key in details:
print(key, end=' ')
# first_name last_name twitter_handle
- Iterating over a String
for key in 'abcdefghi':
print(key, end=' ')
# a b c d e f g h i
- Iterating over a file
for line in open('text.txt'):
print(line)
# A line printed here
More Iterables Consumers
- Many functions consume iterables
- Reductions
- sum(iter)
- min(iter)
- max(iter)
- Constructors
- list(iter)
- tuple(iter)
- dict(iter)
- set(iter)
- in operator
- other libraries such as itertools
- Sequence unpacking
a,b,c = iter([1,2,3])
- Note that you can only go forward in an iterator; there’s no way to get the previous element, reset the iterator, or make a copy of it.
Generators
- Generators are a special class of functions that simplify the task of writing iterators.
- A generator is a function that produces a sequence of results instead of a single value, i.e you generate a series of values.
- A generator is simply a function which returns an object on which you can call next(), such that for every call it returns some value, until it raises a StopIteration exception.
- Such an object is called an iterator.
Making Generators
def Range(start, end):
current = start
while current < end:
yield current
current += 1
raise StopIteration
my_range_generator = Range(2,10)
range_iterator = iter(my_range_generator)
print(Range)
print(my_range_generator)
print('range_iterator is my_range_generator? - ',
range_iterator is my_range_generator)
for val in my_range_generator:
print(val, end=' ')
# <function Range at 0x10f6100d0>
# <generator object Range at 0x10f78bf68>
# range_iterator is my_range_generator? - True
# 2 3 4 5 6 7 8 9
- Use yield in a normal function
Generator Expressions
generating_func = (n for n in range(3, 9) if n > 5)
print(generating_func)
print(next(generating_func))
print(next(generating_func))
print(next(generating_func))
# <generator object <genexpr> at 0x102a0af68>
# 6
# 7
# 8
( expression for expr in sequence1 if condition1 ...
for exprN in sequenceN
if conditionN )
Why Generators?
- When a producer function has a hard enough job that it requires maintaining state between values produced, most programming languages offer no pleasant and efficient solution beyond adding a callback function to the producer's argument list, to be called with each value produced.
Generators vs Iterators
- A generator function is slightly different than an object that supports iteration.
- A generator is a one-time operation. You can iterate over the generated data once, but if you want to do it again, you have to call the generator function again.
- This is different than a list (which you can iterate over as many times as you want )
Advantages of Generators
- Generators make lazy evaluation possible.
- Generators are good for calculating large sets of results where you don't know if you are going to need all results.
- Generators can be used to replace callbacks with iteration. You may occasionally report back to the caller. With generators, you yield when you want to report.
- It is memory efficient since all the data need not be generated at once.
- A non-obvious use of generator lets you do things like update UI or run several jobs "simultaneously" (interleaved, actually) while not using threads. [PEP 255]
Examples
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
import itertools
list(itertools.islice(fib(), 10))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
def fib(n):
a = b = 1
result = []
for i in xrange(n):
result.append(a)
a, b = b, a + b
return result
When not to use Generators?
Use a list instead of a generator when:
- You need to access the data multiple times (i.e. cache the results instead of recomputing them)
for i in outer: # used once, okay to be a generator or return a list
for j in inner: # used multiple times, reusing a list is better
...
for i in reversed(data): ... # generators aren't reversible
s[i], s[j] = s[j], s[i] # generators aren't indexable
- You need random access (or any access other than forward sequential order)
Premature optimization is the root of all evil in programming. - Knuth
- You need to join strings (which requires two passes over the data)
- You are using PyPy which sometimes can't optimize generator code as much as it can with normal function calls and list manipulations.
s = ''.join(data) # lists are faster than generators in this use case
# str.join makes one pass to add-up the lengths of all the string fragments
# so it knows much memory to allocate for the combined final result.
# The second pass copies the string fragments into in
# the new buffer to create a single new string.
# If the input to join isn't a list, it has to
# do extra work to build a temporary list for the two passes.
Implementations
def cycle(iterable):
# cycle('ABCD') --> A B C D A B C D A B C D ...
saved = list(iterable)
while saved:
for element in saved:
yield element
def dropwhile(predicate, iterable):
# dropwhile(lambda x: x<5, [1,4,6,4,1]) --> 6 4 1
iterable = iter(iterable)
for x in iterable:
if not predicate(x):
yield x
break
for x in iterable:
yield x
Thanks
Please feel free to contact me.
@salman_arfat
Generators in Python
By Arfat Salman
Generators in Python
A conceptual overview of generator functions, and how they relate to iterators in Python 3.
- 519