Writing generator functions

Many functions can be expressed neatly as generator expressions. Indeed, we've seen that almost any kind of mapping or filtering can be done as a generator expression. They can also be done with a built-in higher-order function, such as map() or filter(), or as a generator function. When considering multiple statement generator functions, we need to be cautious that we don't stray from the guiding principles of functional programming: stateless function evaluation.

Using Python for functional programming means walking on a knife edge between purely functional programming and imperative programming. We need to identify and isolate the places where we must resort to imperative Python code because there isn't a purely functional alternative available.

We're obligated to write generator functions when we need statement features of Python. Features, such as the following, aren't available in generator expressions:

  • A with context to work with external resources. We'll look at this in Chapter 6, Recursions and Reductions, where we address file parsing.
  • A while statement to iterate somewhat more flexibly than a for statement. An example of this was shown previously in the Flattening data while mapping section.
  • A break or return statement to implement a search that terminates a loop early.
  • The try-except construct to handle exceptions.
  • An internal function definition. We've looked at this in several examples in Chapter 1, Understanding Functional Programming and Chapter 2, Introducing Essential Functional Concepts. We'll also revisit it in Chapter 6, Recursions and Reductions.
  • A really complex if-elif sequence. Trying to express more than one alternative via if-else conditional expressions can become complex looking.
  • At the edge of the envelope, we have less-used features of Python, such as for-else, while-else, try-else, and try-else-finally. These are all statement-level features that aren't available in generator expressions.

The break statement is most commonly used to end processing of a collection early. We can end processing after the first item that satisfies some criteria. This is a version of the any() function used to find the existence of a value with a given property. We can also end after processing a larger number of items, but not all of them.

Finding a single value can be expressed succinctly as min(some-big-expression) or max(something big). In these cases, we're committed to examining all of the values to assure that we've properly found the minimum or the maximum.

In a few cases, we can stand to have a first(function, collection) function where the first value that is True is sufficient. We'd like the processing to terminate as early as possible, saving needless calculation.

We can define a function as follows:

def first(predicate: Callable, collection: Iterable) -> Any:
for x in collection:
if predicate(x): return x

We've iterated through the collection, applying the given predicate function. If the predicate result is True, the function returns the associated value and stops processing the iterable. If we exhaust the collection, the default value of None will be returned.

We can also download a version of this from PyPi. The first module contains a variation on this idea. For more details, visit https://pypi.python.org/pypi/first.

This can act as a helper when trying to determine whether a number is a prime number or not. The following is a function that tests a number for being prime:

import math
def isprimeh(x: int) -> bool:
    if x == 2: return True
    if x % 2 == 0: return False
    factor= first( 
lambda n: x%n==0,
range(3, int(math.sqrt(x)+.5)+1, 2)) return factor is None

This function handles a few of the edge cases regarding the number 2 being a prime number and every other even number being composite. Then, it uses the first() function defined previously to locate the first factor in the given collection.

When the first() function will return the factor, the actual number doesn't matter. Its existence is all that matters for this particular example. Therefore, the isprimeh() function returns True if no factor was found.

We can do something similar to handle data exceptions. The following is a version of the map() function that also filters bad data:

def map_not_none(func: Callable, source: Iterable) -> Iterator:
for x in source: try: yield func(x) except Exception as e: pass # For help debugging, use print(e)

This function steps through the items in the iterable, assigning each item to the x variable. It attempts to apply the function to the item; if no exception is raised, the resulting value is yielded. If an exception is raised, the offending source item is silently dropped.

This can be handy when dealing with data that include values that are not applicable or missing. Rather than working out complex filters to exclude these values, we attempt to process them and drop the ones that aren't valid.

We might use the map() function for mapping not-None values, as follows:

data = map_not_none(int, some_source)  

We'll apply the int() function to each value in some_source. When the some_source parameter is an iterable collection of strings, this can be a handy way to reject strings that don't represent a number.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset