Lambda functions

By Martin McBride, 2024-01-05
Tags: lambda function function objects function
Categories: functional programming


Lambda functions are anonymous, one-line functions, designed to be used in just one place, and declared where they are used.

Motivation

When we create a function in Python, it is often because we have identified a section of code that performs a useful task that can be separated out to become its own entity.

There are 2 reasons for doing this. First, it allows us to reuse the code in numerous places, without duplicating the original code. Secondly, it allows a complex task to be broken down into smaller tasks, making the code easier to understand and maintain.

But Python also supports the functional programming paradigm. In this style of coding, functions are objects in their own right, and we sometimes pass a simple function as a parameter, to control the behaviour of the parent function. Functions that accept or return function objects are sometimes called higher-order functions.

In this style of programming, we often create very simple functions that are only used once, as a parameter to another function. In that case there is no real need to name the function or to comment its use (if it is a simple, obvious function) so the ability to declare the function in-place is very useful.

Lambda function syntax

A lambda function with 1 parameter is declared like this. This example takes a number and returns the cube of that number:

lambda x: x**3

A lambda function with 2 parameters is declared like this. This example takes a string and an integer and returns the nth element of the string:

lambda s, n: s[n]

Lambda functions with more parameters are possible, of course, and follow the same pattern. The general form of a lambda expression is:

lambda arguments: expression

Here lambda is the keyword that introduces a lambda statement, arguments is a list of zero or more arguments, separated by commas, and expression is a single Python expression that will be evaluated when the function is called. Notice that only expressions can be evaluated, not statements. So for example you cannot generally assign a value to a variable or use coding techniques such as if statements, try statements and similar. There are a few ways around these restrictions, as we will see later, but they are quite limited.

It is also possible to define a lambda function that has no parameters. That is quite rare, and we normally use lambda functions to transform values, but here is an example:

lambda: random.randrange(10)

This lambda will return a random integer between 0 and 9. You will need to import the standard random library.

Uses of lambda functions

Lambda functions have many different uses. A couple of common examples are higher-order functions, and closures.

Lambda functions and higher-order functions

A common use of lambda functions is when we need to pass a simple function into a higher-order function such as map. The map function applies a supplied function to an iterator and returns an iterator result. For example:

k = [1.1, -2.2, 3.6]

def int_abs(x):
    return int(abs(x))

m = map(int_abs, k)

print(list(m))

This code defines a function int_abs that takes the absolute value of a number and then converts it to an integer. It then uses the map function to apply int_abs to every value in the list k.

The int_abs function is very simple and is only used once, inside the map call. The whole process of defining a function for such a simple, single-use seems like overkill and clutters up the code with pointless boilerplate code. We can replace it with an in-place lambda function:

k = [1.1, -2.2, 3.6]

m = map(lambda x: int(abs(x)), k)

print(list(m))

I am not a great advocate of jumping through hoops to make code shorter for the sake of it. But in my opinion, this use of lambda makes the code shorter and clearer, which is worth doing.

Other well-known higher-order functions that can benefit from lambdas include filter, sort and reduce (from the functools module).

Lambda functions and closures

Another use of lambdas is to create simple closures. A closure is a form of function factory where a factory function returns a specialised inner function, usually with certain parameters baked in. An example from the main closures article is composition:

def compose(f, g):
    def comp(x):
        return f(g(x))
    return comp

This gives us an alternative way to implement our int_abs function, like this:

int_abs = compose(int, abs)

But we can improve the original compose function. In site the function we create an inner function comp. This function is very simple, and only used once, so why not use a lambda here too?

We could do this as a first step:

def compose(f, g):
    comp = lambda x: f(g(x))
    return comp

This is better, but of course, we don't need to store the lambda in a variable before returning it, we can just do this:

def compose(f, g):
    return lambda x: f(g(x))

So here is our complete solution (the equivalent of what we did before):

k = [1.1, -2.2, 3.6]

def compose(f, g):
    return lambda x: f(g(x))

m = map(compose(int, abs), k)

print(list(m))

Now this is slightly more code than before, but it could be argued that explicitly composing int and abs is more declarative than the bare lambda function.

What we should also remember is that the compose function is general purpose. We can use it in other places too. In the end, the best solution depends on the situation and, to a large extent, is a matter of personal preference.

Making the most of one line

As lambda function can only contain a single expression in its body. This means that it can perform calculations, but it can't generally include things like variable assignments, if statements, or loops, because they are statements rather than expressions. However, there are potential workarounds for some of these.

Variable assignment

This is not allowed because we can't use an assignment statement:

j = 0

lambda x: (j = x) # ERROR

Notice that the brackets as required because the lambda keyword has a higher priority than the equals operator. Even with the brackets, this code gives a syntax error because statements aren't allowed within a lambda.

Python 3.8 introduced assignment expressions (the so-called "walrus operator"). This code is allowed because the walrus operator is an expression, not a statement:

j = 0

lambda x: (j := x) # Fail - creates a local j

However, this still doesn't work. It creates a local variable j so the outer variable isn't updated.

If you really must get a value out of the lambda function, you might try making j a list and appending a value to it:

j = 0

lambda x: j.append(x)

This works, every time the lambda is called the value x is added to the list j. But you should consider whether a lambda is the best technique to use if you find yourself writing code like this.

If statements

You can't generally use if statements in a lambda expression because it is a statement, not an expression. However, you can use the ternary operator, which is a kind of one-line if expression:

lambda x: x if x > 0 else 0

This function will return x if it is a positive value, otherwise 0.

Loops

A lambda expression cannot include a for loop because that is a statement. However, it can include a list cmprehension which can do a similar job in some circumstances:

lambda x: [i for i in range(x)]

This function will return a list of numbers 0 to x - 1 when passed an integer x.

Alternatives to lambda functions

There are several alternatives that you might consider instead of defining a lambda function.

First, it is always worth checking if there are any existing functions that you could use instead. For example, we can somtimes use built-in functions:

lambda x, y: x if x > y else y # Use built-in max function instead

There are also many useful functions in the operator library:

lambda x, y: x * y # Use operator.mul instead

We can also use partial application. for example, suppose we wanted to write a map call that adds 2 to every item in a list. We could do it like this:

k = [1, -2, 3]

m = map(lambda x: 2 + x, k)

print(list(m))

This creates a lambda function that adds 2 to the parameter x. However, there is an alternative, the partial function of the standard functools library. It works like this:

from functools import partial
from operator import add

k = [1, -2, 3]

m = map(partial(add, 2), k)

print(list(m))

We first import the partial function. We also import add from the operator library. add is the function version of the + operator. The add function accepts two parameters a and b, and returns the sum of the two.

When we apply partial with a parameter of 2, it creates a brand new anonymous function. The new function is like the add function, except that the value of the first parameter a is preset to 2. The new function only accepts 1 argument, b, and returns 2 + b. This, of course, is exactly what we want.

Lambda function antipattern

You will sometimes see code like this:

double = lambda x: x*2

This code defines an anonymous function, and then gives it a name! While this works, it defeats the whole purpose of using an anonymous function.

There are 2 possibilities. If you are only using the function once, then declare the lambda where it is used, so you don't need to give the function a name. If you are using the function more than once, then there is no point in declaring it as a lambda function. Just declare it as a regular function, like this:

def double(x):
    return x*2

Yes, it takes one extra line, but that rarely matters. What matters more is that the code is clear and maintainable. There are several advantages to using def in this case:

  • It is what other developers expect. If they see double() called somewhere, they will be expecting a regular definition, and might not spot a named lambda.
  • It could potentially confuse the IDE, making it more difficult to navigate the code.
  • Most projects have a standard way of documenting functions and often generate documentation automatically. A named lambda is more likely to be left undocumented, and even if it is it might not appear correctly in the automatic documentation.
  • If the function needs to be extended in the future, it will probably need to be converted into a regular function. For example, if the function needs an extra line, or if you want to add additional optional arguments, it will need conversion because lambdas don't support those things.

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