Creating Functional Functions

PythonPythonBeginner
Practice Now

This tutorial is from open-source community. Access the source code

Introduction

This section introduces the idea of using functions to create other functions.

Introduction

Consider the following function.

def add(x, y):
    def do_add():
        print('Adding', x, y)
        return x + y
    return do_add

This is a function that returns another function.

>>> a = add(3,4)
>>> a
<function add.<locals>.do_add at 0x7f27d8a38790>
>>> a()
Adding 3 4
7

Local Variables

Observe how the inner function refers to variables defined by the outer function.

def add(x, y):
    def do_add():
        ## `x` and `y` are defined above `add(x, y)`
        print('Adding', x, y)
        return x + y
    return do_add

Further observe that those variables are somehow kept alive after add() has finished.

>>> a = add(3,4)
>>> a
<function do_add at 0x6a670>
>>> a()
Adding 3 4      ## Where are these values coming from?
7

Closures

When an inner function is returned as a result, that inner function is known as a closure.

def add(x, y):
    ## `do_add` is a closure
    def do_add():
        print('Adding', x, y)
        return x + y
    return do_add

Essential feature: A closure retains the values of all variables needed for the function to run properly later on. Think of a closure as a function plus an extra environment that holds the values of variables that it depends on.

Using Closures

Closure are an essential feature of Python. However, their use if often subtle. Common applications:

  • Use in callback functions.
  • Delayed evaluation.
  • Decorator functions (later).

Delayed Evaluation

Consider a function like this:

def after(seconds, func):
    import time
    time.sleep(seconds)
    func()

Usage example:

def greeting():
    print('Hello Guido')

after(30, greeting)

after executes the supplied function... later.

Closures carry extra information around.

def add(x, y):
    def do_add():
        print(f'Adding {x} + {y} -> {x+y}')
    return do_add

def after(seconds, func):
    import time
    time.sleep(seconds)
    func()

after(30, add(2, 3))
## `do_add` has the references x -> 2 and y -> 3

Code Repetition

Closures can also be used as technique for avoiding excessive code repetition. You can write functions that make code.

Exercise 7.7: Using Closures to Avoid Repetition

One of the more powerful features of closures is their use in generating repetitive code. If you refer back to Exercise 5.7, recall the code for defining a property with type checking.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price
    ...
    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value
    ...

Instead of repeatedly typing that code over and over again, you can automatically create it using a closure.

Make a file typedproperty.py and put the following code in it:

## typedproperty.py

def typedproperty(name, expected_type):
    private_name = '_' + name
    @property
    def prop(self):
        return getattr(self, private_name)

    @prop.setter
    def prop(self, value):
        if not isinstance(value, expected_type):
            raise TypeError(f'Expected {expected_type}')
        setattr(self, private_name, value)

    return prop

Now, try it out by defining a class like this:

from typedproperty import typedproperty

class Stock:
    name = typedproperty('name', str)
    shares = typedproperty('shares', int)
    price = typedproperty('price', float)

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Try creating an instance and verifying that type-checking works.

>>> s = Stock('IBM', 50, 91.1)
>>> s.name
'IBM'
>>> s.shares = '100'
... should get a TypeError ...
>>>

Exercise 7.8: Simplifying Function Calls

In the above example, users might find calls such as typedproperty('shares', int) a bit verbose to type--especially if they're repeated a lot. Add the following definitions to the typedproperty.py file:

String = lambda name: typedproperty(name, str)
Integer = lambda name: typedproperty(name, int)
Float = lambda name: typedproperty(name, float)

Now, rewrite the Stock class to use these functions instead:

class Stock:
    name = String('name')
    shares = Integer('shares')
    price = Float('price')

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Ah, that's a bit better. The main takeaway here is that closures and lambda can often be used to simplify code and eliminate annoying repetition. This is often good.

Summary

Congratulations! You have completed the Returning Functions lab. You can practice more labs in LabEx to improve your skills.

Other Python Tutorials you may like