Generators are implemented with syntax very similar to functions, but instead of returning values, a yield statement is executed to indicate each element in the series
Consider the goal of computing all factors of a positive integer. A traditional function might return a list contating all factors, implemeted as follows:
def factors(n): # traditional function that computes factors
results = [] # store factors in a new list
for k in range(1, n+1):
if n % k == 0: # divides evenly, thus k is a factor
results.append(k) # add k to the list of factors
return results # return the entire list
In contrast, an implementation of a generator for computing those factors could be implemented as follows
def factors(n): # generator that computes factors
for k in range(1, n+1):
if n % k == 0: # divides evenly, thus k is a factor
yield k # yield this factor as next result
when the flow of control naturally reaches the end of the procedure or a zero-argument return statement, a StopIteration exception is automatically raised.
An iterator can relly on multiple yield statements in different constructs, with the generated series determined by the natural flow of control. For example, a more efficient version of the above generator:
def factors(n): # generator that computes factors
k = 1
while k * k < n: # while k < sqrt(n)
if n % k == 0:
yield k
yield n // k
k += 1
if k * k == n: # special case if n is a perfect square
yield k