Outline

Understanding the functional programming features of Python can help you to quickly add features and functionality to your module or application. Remembering that Python supports the concept of First Class functions will help you align your thinking for this section. A function is an instance of the Object Type. You can store the function in a variable, pass the function as a parameter to another function, return the function from a function, and you can store them in data structures such as hash tables, lists, etc.

Let's create some simple Python files and play around with some of the functional programming features. You can use the terminal or your chosen editor. I'm using Visual Studio Code with the Python extension installed. Using Shift+Cmd+p and typing Python: Select Interpreter, I chose my working project version of Python 3.7.2. Then created an integrated terminal by hitting Shift+Cmd+p and typing Terminal: Create new Integrated terminal.

I've created a file called functions.py and placed the following code in it to show that we can pass a function as an object in a parameter:

def add(x, y):
    return x + y


def subtract(x, y):
    return x - y


def do_binary_op(op, x, y):
    return op(x, y)

Then, in the integrated terminal prompt, I enter python Using the Python Interpreter:

$ python
Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 24 2018, 02:44:43) 
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from functions import add, subtract, do_binary_op
>>> do_binary_op(subtract, 10, 3)
7
>>> do_binary_op(add, 10, 5)
15

Functions in Python can be referred to by name and get executed only when you call them with parenthesis.

Note: You can import the functions using * like: from functions import *. This can be handy when you are playing around with the interpreter but NOT recommended for production bound code. Using it would make it impossible to control changes to the modules you are importing and could have unintended results.

Functions are objects

There are a couple of ways that we can these assignments of functions into variables.

def shout(text):
    return text.upper()

print(shout('Hello'))

yell = shout

print(yell('Hello'))

Take some time to understand Decorators

A decorator is relatively complicated the first time you look at them. They, however, are a highly useful feature of Python. It is important to remember that functions are objects that can be assigned to variables and passed to and from other functions. Add this to the ability to define a child function within a function (a closure) and capture the parent function's local state is what can make them so powerful.

I've created a file called decorators.py and placed the following code from the write-up in it:

def bread(func):
    def wrapper():
        print("</''''''\>")
        func()
        print("<\______/>")
    return wrapper


def ingredients(func):
    def wrapper():
        print('#tomatoes#')
        func()
        print('~salad~')
    return wrapper


@bread
@ingredients
def sandwich(food='--ham--'):
    print(food)

Now from your terminal window, import decorators and call the sandwich() function.

$ python
>>> from decorators import *
>>> sandwich()
</''''''\>
#tomatoes#
--ham--
~salad~
<\______/>
>>> 

You can decorate the sandwich function with the bread and ingredients by using @bread then @ingredients above it. This would be the same as sandwich = bread(ingredients(sandwich)). We are trying to get away from those types of assignments by using decorators. The order of the decorators is important as well. Play around with switching the order and see how your sandwich ends up.

Functions

Functions are declared with the def keyword. Arguments follow the function name. Default values are specified using an = after the argument. They allow us to group related statements to perform a specific task. Functions allow our code to be reusable and helps to manage our code.

Function names follow the same naming convention as variable names. Function names should be lowercase, with words separated by underscores.

Using the argument name when calling a function allows you to provide them in any order.

def create_super_hero(superName, hiddenIdentity, alignment):
    return (
        f'Creating a new character named {superName}, '
        f'whos identity will be {hiddenIdentity} in the public world. '
        f'This superhero will be {alignment}.'
        )


print(create_super_hero(
    alignment='Evil',
    superName='Bane',
    hiddenIdentity='Secret Identity'
    ))

Functions can contain a return statement which is used to exit the function and return to the calling code's next statement.


*args and **kwargs

I had a hard time figuring out what these were the first time I saw them. *args and *kwargs can be used in function definitions which allow you to pass any number of arguments to a function. *args is used when you are passing parameters without keywords (non-dictionary). It's a Python convention to use these names. You can use whatever variable name you want. The important piece is the *. You can use *foo, **bar if you want.

def superHeroTest(firstArg, *args):
    print(f'this is the first argument value: {firstArg}')

    for arg in args:
        print(f'argument value in *args: {arg}')

superHeroTest('Batman', 'Superman', 'Green Lantern', 'James Gordon')

# Output
# this is the first argument value: Batman
# argument value in *args: Superman
# argument value in *args: Green Lantern
# argument value in *args: James Gordon

If you want to allow named arguments to be passed to your function, use **kwargs. Again, you get the same functionality as *args, allowing you to pass any number of keyworded arguments. For example:

def superHeroTest2(**kwargs):
        for key, value in kwargs.items():
                print(f'{key} = {value}')

superHeroTest2(name='Batman', favColor='really really dark grey')
# Output
# name = Batman
# favColor = really really dark grey
 

I finished! On to the next chapter