Function objects and lambdas

By Martin McBride, 2023-12-30
Tags: function function object lambda function map
Categories: python language intermediate python

One of the first things you learn to do in Python is to call functions, for example, you might have used print and input functions to create a simple "What is your name" type program. You will also have learned how to create your own functions using the def operator.

There is one more important thing to know about functions in Python - a function is an object, just like an integer, a string or a list. This means, for example:

  • You can store a function in a variable.
  • You can store a collection of functions in a list or dictionary.
  • You can pass a function into another function as a parameter.
  • A function can return another function as its return value.

Functions are first-class objects in Python - this simply means that function objects are just like any other objects, with no special restrictions on their use. This feature allows Python to support the functional programming paradigm.

This tutorial doesn't cover functional programming in depth, it mainly looks at some practical uses of function objects. See the functional programming articles for more details.

We will also meet lambda functions aka anonymous functions.

All functions are objects

This code defines a simple function and then prints its type

def square(x):
    return x*x


The result is

<class 'function'>

This tells us that the object square is of type function. Notice that we take the type of square, the variable that holds the object, rather than square() (that would simply call the function).

Since square holds an object, we can assign it to another variable:

sq = square

Now sq and square both hold the same function object.

Applying a function

So now that we have a variable sq that holds a function object, how do we call the function? This is often called applying the function. Well in Python it is very easy. You just use the normal function calling syntax:

x = sq(2)

The name sq is the variable that holds the function pointer. When we use the () operator, it applies the function to the value passed in (ie it calls the function with the value 2).

We sometimes say that sq is an alias of square, but all this means is that the two variables both refer to the same function object.

Passing a function as a parameter

In the next example, we will create a function that takes a list of names and prints them out. The names are stored as tuples containing the first name and last name. The function print_names prints the names out, one per line.

However, we would like to be able to control how the names are formatted. So we make our print_names function accept a formatter function object. The formatter function accepts a name tuple and returns a string.

As an example, the format_space function joins the first and last names, with a space between them:

names = [ ('John', 'Smith'),
          ('Anne', 'Jones'),
          ('Robert', 'Davis')]

def print_names(names, formatter):
    for name in names:

def format_space(name):
    return ' '.join(name)

print_names(names, format_space)

As you might expect, this creates the following output:

John Smith
Anne Jones
Robert Davis

Now suppose we wanted to print a list of the names in a different format, say surname plus initial. We can do this without changing the print_names function, simply by writing a new formatter function and passing that into the call:

def format_initial(name):
    return f"{name[1]}, {name[0][0]}"

print_names(names, format_initial)

This prints the second part of the name, plus a comma, plus the first character of the first name, giving this output:

Smith, J
Jones, A
Davis, R

Finally, what if we wanted to print out the email addresses - formed from the lowercase names, separated by a dot, followed by the domain name? Like this:

def format_email(name):
        return f"{name[0]}.{name[1]}"

print_names(names, format_email)

This gives:

As you can see, using a function object allows us to change the behaviour of the print_names function without changing its code. It is a very powerful technique.

Higher order functions

A higher-order function is a function that operates on functions - it either accepts function objects as parameters, returns a function object, or both.

The map() function

One example of a higher-order function is the built-in map function. This takes a function object and applies it to a sequence of inputs. Here is an example using a square function:

def square(x):
    return x*x

numbers = [1, 3, 5, 7]

result = map(square, numbers)

The map function applies the square function to each element in numbers, and returns the result. Note that the result is a map object, which is a lazy operator. You can iterate over a map object, for example in a for loop. But if you want to print the result out as a list of values, you must first convert the lazy sequence into an actual list (using the list function). The result is a list of the squares of each input value:

[1, 9, 25, 49]

Functions that return functions

Python functions have another powerful feature. A higher-order function can transform one function into another, related function. For example:

def twice(f):
    def g(x):
        return f(f(x))
    return g

The function twice accepts a parameter f, which must take a single parameter.

Inside the function twice, we define a nested function g that calculates f of f of x - that is, it applies f twice. The function g is called an inner function.

But here is the clever bit. twice returns the function g, which is a brand new function that apples f twice, whatever f might be!

So if we have an increment function that adds 1 to x, we can create an inc2 function that adds 1 to x and then adds 1 to the result (that is, it adds 2 to x). This code prints 7:

def increment(x):
    return x + 1

inc2 = twice(increment)

Similarly, if we have a square function, we can create an sq2 function that squares x and then squares the result (that is, it raises x to the power 4). This prints 81:

def square(x):
    return x*x

sq2 = twice(square)

Notice that the inc2 function remembers that the function passed into twice was the increment function. And also the sq2 function remembers that the function passed into twice was the square function. A function that retains the values of its local variables and make sthem available to an inner function after the main function has returned is called a closure.

Lambda functions

In the map example above, we have defined a function square simply so that we can use it in the call to the map function:

def square(x):
    return x*x

numbers = [1, 3, 5, 7]

result = map(square, numbers)

Since we only use the square function in one place, it seems a bit pointless to have to declare it and give it a name. It would be a lot simpler just to be able to specify the function where we use it, in the map call. That is what lambda functions are for.

Despite sounding quite exotic, a lambda function is simply a neat bit of syntax you can use to create small simple functions in line, without the hassle of creating a full function definition, and without needing to bother giving it a name (it is an anonymous function).

Here is how it is used:

numbers = [1, 3, 5, 7]

result = map(lambda x: x*x, numbers)

This is the bit of code that defines the function:

lambda x: x*x

It creates a function object that takes a parameter x and returns the value x squared. The function object isn't given a name because it is used in place, passed directly into the map call.

In the case of the twice function, lambdas can really simplify things. Here is the original code:

def twice(f):
    def g(x):
        return f(f(x))
    return g

We can replace the inner function g with a lambda (the name g isn't used outside the function anyway), giving us this:

def twice(f):
    return lambda x: f(f(x))

Not only is this shorter, but it is also a clearer expression of what is going on. twice returns a function that has the effect of applying f twice.

See also

If you found this article useful, you might be interested in the book NumPy Recipes or other books by the same author.

Join the PythonInformer Newsletter

Sign up using this form to receive an email when new content is added:

Popular tags

2d arrays abstract data type alignment and angle animation arc array arrays bar chart bar style behavioural pattern bezier curve built-in function callable object chain circle classes clipping close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data science data types decorator design pattern device space dictionary drawing duck typing efficiency ellipse else encryption enumerate fill filter font font style for loop formula function function composition function plot functools game development generativepy tutorial generator geometry gif global variable gradient greyscale higher order function hsl html image image processing imagesurface immutable object in operator index inner function input installing iter iterable iterator itertools join l system lambda function latex len lerp line line plot line style linear gradient linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map marker style matplotlib monad mutability named parameter numeric python numpy object open operator optimisation optional parameter or pandas partial application path pattern permutations pie chart pil pillow polygon pong positional parameter print product programming paradigms programming techniques pure function python standard library radial gradient range recipes rectangle recursion reduce regular polygon repeat rgb rotation roundrect scaling scatter plot scipy sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template tex text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip zip_longest