A brief analysis of the use methods of Python yield

  • 2020-06-01 10:13:18
  • OfStack

How do I generate Fibonacci columns

The column of Fibonacci (Fibonacci) is a very simple recursive sequence. Except for the first and second Numbers, any number can be obtained by adding the first two Numbers together. Using a computer program to output the number of pre-N in the Fibonacci list is a very simple problem, and many beginners can easily write the following functions:

Listing 1. Simple output Fibonacci and list the number of N before it


 def fab(max): 
  n, a, b = 0, 0, 1 
  while n < max: 
    print b 
    a, b = b, a + b 
    n = n + 1

By executing fab(5), we can get the following output:

> > > fab(5)
1
1
2
3
5

The results are fine, but experienced developers will point out that printing Numbers in fab directly with print results in poor reusability of the function, because fab returns None and no other function can obtain the sequence generated by the function.

To improve the reusability of the fab function, it is best not to print out the sequence directly, but to return one List. The second version of the fab function is as follows:

Listing 2. Output Fibonacci and list the number of N before version 2


 def fab(max): 
  n, a, b = 0, 0, 1 
  L = [] 
  while n < max: 
    L.append(b) 
    a, b = b, a + b 
    n = n + 1 
  return L

The List returned by the fab function can be printed as follows:

> > > for n in fab(5):
... print n
...
1
1
2
3
5

The rewritten fab function satisfies the requirements of reusability by returning List, but more experienced developers will point out that the memory footprint of the function will increase with the increase of the parameter max. If you want to control the memory footprint, it is better not to use List to save the intermediate results, but to iterate through the iterable object. For example, in Python2.x, the code:

Listing 3. Iterate through the iterable object

 for i in range(1000): pass Will result in the generation of 1 1000 elements of List, while the code:

 for i in xrange(1000): pass Instead of generating an List of 1000 elements, the next value is returned in each iteration with a small footprint. Because xrange does not return List, it returns an iterable object.

Using iterable, we can rewrite the fab function to 1 class that supports iterable. The following is the Fab of the third version:

Listing 4. Version 3


class Fab(object): 

  def __init__(self, max): 
    self.max = max 
    self.n, self.a, self.b = 0, 0, 1 

  def __iter__(self): 
    return self 

  def next(self): 
    if self.n < self.max: 
      r = self.b 
      self.a, self.b = self.b, self.a + self.b 
      self.n = self.n + 1 
      return r 
    raise StopIteration()

Class Fab keeps returning the next number in the sequence through next(), and the memory footprint is always constant:

> > > for n in Fab(5):
... print n
...
1
1
2
3
5

However, with this version of class, the code is far less concise than the fab function from version 1. yield comes in handy if we want to keep the first version of the fab function simple while still getting the effect of iterable:

Listing 5. Using version 4 of yield


 def fab(max): 
  n, a, b = 0, 0, 1 
  while n < max: 
    yield b 
    # print b 
    a, b = b, a + b 
    n = n + 1 

'''

In the fourth version of fab, compared with the first version, only print b was changed to yield b, which achieved the effect of iterable while keeping it simple.

Call version 4 fab and version 2 fab full 1 to:

> > > for n in fab(5):
... print n
...
1
1
2
3
5

Simply put, the purpose of yield is to change a function into an generator. A function with yield is no longer a normal function. The Python interpreter will treat it as an generator. In for loop executes, each loop will execute fab functions within the code, and performs to yield b, fab function returns a value iteration, the next iteration, code from yield b under 1 statement to continue, and the function of the local variable seems to interrupt execution and the last is completely 1 sample before, so the function to continue, until meet yield again.

You can also manually call the next() method of fab(5) (because fab(5) is an generator object that has the next() method), so that you can see the fab execution flow more clearly:

Listing 6. Executing the process


 >>> f = fab(5) 
 >>> f.next() 
 1 
 >>> f.next() 
 1 
 >>> f.next() 
 2 
 >>> f.next() 
 3 
 >>> f.next() 
 5 
 >>> f.next() 
 Traceback (most recent call last): 
 File "<stdin>", line 1, in <module> 
 StopIteration

When the function completes, generator automatically throws an StopIteration exception to indicate that the iteration is complete. In the for loop, you do not need to handle the StopIteration exception and the loop ends normally.

We can draw the following conclusions:

A function with yield is an generator, which, unlike normal functions, generates an generator that looks like a function call, but does not execute any function code until it is called next() (which is automatically called next() in the for loop). Although the execution process is still executed according to the process of the function, every time an yield statement is executed, it is interrupted and an iteration value is returned, which is continued from the next statement of yield at the next execution. It looks as if a function was interrupted several times during normal execution by yield, with each interrupt returning the current iteration value via yield.

The benefits of yield are obvious. If you change one function to one generator, you will gain the ability to iterate. Compared with the method of saving the state of a class instance to calculate the value of one next(), not only the code is concise, but also the execution process is extremely clear.

How do I determine if a function is a special generator function? isgeneratorfunction can be used to judge:

Listing 7. Use isgeneratorfunction to judge


 >>> from inspect import isgeneratorfunction 
 >>> isgeneratorfunction(fab) 
 True

Note the difference between fab and fab(5), fab is one generator function, and fab(5) is one generator returned by a call to fab, such as the difference between a class definition and an instance of a class:

Listing 8. Class definition and class instance


 >>> import types 
 >>> isinstance(fab, types.GeneratorType) 
 False 
 >>> isinstance(fab(5), types.GeneratorType) 
 True
fab  You can't iterate  fab(5)  Is iterative: 
 >>> from collections import Iterable 
 >>> isinstance(fab, Iterable) 
 False 
 >>> isinstance(fab(5), Iterable) 
 True

Each time the fab function is called, a new generator instance is generated. Each instance does not affect each other:


>>> f1 = fab(3) 
 >>> f2 = fab(5) 
 >>> print 'f1:', f1.next() 
 f1: 1 
 >>> print 'f2:', f2.next() 
 f2: 1 
 >>> print 'f1:', f1.next() 
 f1: 1 
 >>> print 'f2:', f2.next() 
 f2: 1 
 >>> print 'f1:', f1.next() 
 f1: 2 
 >>> print 'f2:', f2.next() 
 f2: 2 
 >>> print 'f2:', f2.next() 
 f2: 3 
 >>> print 'f2:', f2.next() 
 f2: 5

The role of return

In 1 generator function, if there is no return, the function will be executed by default until the function is finished; if return is executed, StopIteration will be directly thrown to terminate the iteration.

Another example

Another example of yield comes from file reading. If the read() method is called directly on the file object, it can result in unpredictable memory usage. A good approach is to use a fixed-length buffer to read the contents of the file over and over again. With yield, we no longer need to write an iterative class to read the file, so we can easily read the file:

Listing 9. Another example of yield


 def read_file(fpath): 
  BLOCK_SIZE = 1024 
  with open(fpath, 'rb') as f: 
    while True: 
      block = f.read(BLOCK_SIZE) 
      if block: 
        yield block 
      else: 
        return


Related articles: