OU blog

Personal Blogs

ExLibris

Algorithms 5: Functions as entities

Visible to anyone in the world
Edited by Martin Thomas Humby, Thursday, 30 July 2015, 20:28

Variables as functions

In common with many current languages, Java being the exception, Python functions can be assigned to a variable. If we get fed up with typing result = new_total(old_total, operator, number) for example, we could write

ntot = new_total
new_tot = ntot(old_tot, opr, num)

Note that when a function is assigned only the function name is used, no brackets, but when the function is called using the variable, the variable name is used as a function name. The brackets and the argument list if there is one distinguish a function call from the function itself.

Python functions are first-class data types which means they can be assigned to variables and passed to other functions. Passing one of a number of different functions can tailor functionality to suit differing requirements when called by the recipient. Storing functions as elements of a data structure these functions can be called to handle events in a way relating to the requirements of a particular instance of the structure.

On a form in a graphical user interface for example, functions supplied by the form and assigned to controls like buttons can dictate what happens when a particular button is clicked. The possibilities got from passing and storing diverse elements of functionality defined by functions are endless.


Functions that return functions

When a function is passed as an argument the receiving function can of course return it. Alternatively a function can return another function that has been defined in same file or imported to it but commonly a function will define a function nested within its body and return that. A function so defined has read-only access to the local variables of the enclosing function including its parameters. It cannot assign to these variables but can modify any mutable types the locals reference.

A nested function can be called within the enclosing function, returned by it and/or passed as an argument to another function. Whether or not a recipient calls the function it receives is of course its option but if it does the passed function can extend the recipient's functionality in some way perhaps based on the values of the callers locals.

Below, passer() defines a nested function lister() and passes it to user()

def user(lister):
new_list = lister()
new_list[0] = 33
print(new_list)

def passer(a_list):
    def lister():
        return a_list[:] # returns a copy
    user(lister)
    return lister

a_list = [1, 2, 3, 4]
a_lister = passer(a_list)
print(a_list) # check original list unaffected
a_list[3] = 66
user(a_lister)

output:
[33, 2, 3, 4]
[1, 2, 3, 4]
[33, 2, 3, 66]

The function passer() demonstrates the two main options: it defines lister(). A lister()instance is passed to user()as an argument to the call and also returned by passer(). To avoid any possibility of modification of the original list, lister() returns a copy. This copy is not made until lister() is called by user. If it was never called no copy would be made.

Doing this implements a strategy called lazy evaluation: functions rather than the results of functions are passed and not called until their result is required. If on occasion the logic in the recipient does not require the function's return value, it is not called. The enclosing function's locals supply a means of passing values to such functions. Here passer()'s local a_list remains in existence after passer() exits and will stay so while any reference to this particular lister() instance remains.

After checking a_list has not been modified main code makes a modification to the list and calls user(). As can be seen from output main code's modification is reflected to the reference to the list retained by lister().

The name of the nested function is never made public and, because it is quite simple, it could be replaced with an anonymous lambda expression:

def passer(a_list):
lister = lambda : a_list[:] # returns a copy
user(lister)
    return lister

The syntax of Python lambdas is quite different from standard functions. The keyword lambda is followed by a comma-delimited list of zero or more parameters separated by a colon from an expression that supplies the lambda's return value:

def add_delta(delta):
expr = lambda x, y: (x + y)/2 + delta
    return expr

Requiring only a single line of code to define and return the required function lambda expressions can be placed inline:

def implies(P, Q):
    if P: return Q()
    else: return True

# in client code that uses implies():
... and implies(workday, lambda : percent_done(today) >= 95) and ...

In implies(P, Q) execution of Q() is conditional on P being true. If P then the lambda is called and only then will percent_done() be called with the value of today that accompanies the lambda. Perhaps percent_done()will call another function, request user input or access a database. A great deal of processing power can be saved using this strategy. 

Nested functions can be seen as existing in an environment - the enclosing-function locals used by the nested function. Together this environment and the nested function form a structure known as a closure.  As for any other structure, a closure can be returned by the enclosing function, assigned to variables or passed to other functions.

[Algorithms 5: Functions as entities    (c) Martin Humby 2015]

Permalink Add your comment
Share post