Introduction
Lambdas: Python’s anonymous functions
Python lambdas are compact, anonymous functions, which are subject to a more restrictive but concise syntax than the regular def
Python functions. They are often greeted with trepidation, given the connotations with lambda calculus in mathematics - however, they are in fact perfectly straightforward and open the door to some really interesting functional programming design ideas!
Let’s get right to the point - the basic general syntax for Python lambdas
is:
lambda argument_1, argument_2, ..., argument_n: expression
And so immediately we notice the following syntax details:
-
There is no body to the function i.e. no statement block. Instead a
lambda
body is restricted to a single expression. This single expression isn’t surrounded by the{
and}
braces but is instead deliberately “exposed” in order to limit the amount of logic you can fit into thelambda
. This means for example that you are effectively forced to uselambda
for simple, straightforward, single task functionality - likewise to steer away from even slightly complex constructs such as theif
statement. -
The single code construct is an expression, not a statement. Consequently a
lambda
can appear in places where the traditionaldef
function statement, for example as an item inside a list literal, or as adef
function signature. Finally it of course also means that you are creating and referencing thelambda
function in place within the code - rather than with thedef
statement which must have already been created elsewhere.
And so lets have a quick comparison of how a traditional def
function statement compares with a lambda
expression with a more concrete example:
Compare:
def myFunc(x, y, z):
return x + y + z
to it’s (named) lambda
equivalent:
myFunc = lambda x, y, z: z + y + z
or it’s immediately invoked (see below) lambda
equivalent:
(lambda x, y, z: x + y + z)(x, y, z)
What we see is that with a lambda
expression we have a much shorter single line expression of code (a so called ‘reduction’ in mathematical terms), that can perform the same functionality and achieve the same output as the traditional def
function statement. Moreover, it can be defined precisely where it is needed -and- if it the result is needed immediately the result of the lambda
can be immediately invoked.
The significance may still be somewhat abstract, but for now consider that this introduces aspects of functional languages (with their origin in mathematics) into Python, a language that is in the main imperative. With functional languages we can make use of a more declarative approach to programming - one that emphasizes abstraction, data transformation, composition and which is stateless. In comparison, imperative languages such as Python, typically embrace the state-based model of computation where program flow is driven stepwise from instruction to instruction and with state built upon (and/or mutated) as the program progresses. The lambda
expression (and also the map()
, filter()
and reduce()
expressions) therefore mark the adoption of very different coding design principles to those the Python language uses in the main.
Syntax Rules
Before we get further into a comparison between def
function statements and lambda
expressions, let’s cover the basic syntax rules that we must follow with lambda
.
-
A
lambda
expression cannot include statements (such asassert
orreturn
). -
As mentioned above,
lambda
expressions must be contained as a single line of execution - no statement blocks! -
Since
lambda
expressions are by definition anonymous functions they do not support type annotations. -
A
lambda
expression can be immediately invoked - just like a JavaScript “Immediately Invoked Function Expression” (IIFE).
1. No Statements
As noted above, a lambda
expression cannot include any statements (such as assert
or return
). Doing so will raise a SyntaxError exception.
For example, the output of the following code snippet would be a SyntaxError
:
myFunc = lambda x: return x
print(myFunc(42))
File "<input>", line 1
(lambda x: return x)(42)
^
SyntaxError: invalid syntax
We notice that the interpreter has identified a SyntaxError
while parsing the code because it involves the return
statement in the body of the lambda.
2. Single Expressions Only
lambda
expressions must be contained as a single line of execution. That is, in contrast to a normal def
function, a Python lambda function is a single expression. In this way lambda
expression provide us a more concise way of defining a function.
Of course, you can still have that single expression spread over several lines by using either parentheses or a multiline string. The result still remains a single expression:
(lambda x:
... (x % 2 and 'odd' or 'even'))(3)
'odd'
The example above runs correctly, despite being spread across two lines, because it is contained in a set of parentheses. But, again, it remains a legal single expression lambda
.
3. Type Annotations
4. Immediately Invoked Function Expressions (IIFE)
A code pattern more common to more functional languages such as JavaScript is to immediately execut a Python lambda function.
We’ve already seen an example of this above:
(lambda x, y, z: x + y + z)(1, 2, 3)
This will both define and then immediately call the lambda
function with the three arguments 1
, 2
, and 3
. Outside of the Python interpreter, this feature is probably not used in practice. Indeed Python does not encourage using immediately invoked lambda expressions. It simply results as a direct consequence of a lambda expression being callable, unlike the body of a normal function.
Uses for lambda
- or - “how not to obfuscate your Python code”
The Python lambda
expression may seem a little redundant in the context of the expressive code structure Python usually encourages. Some of the arguments against Python lambda
expression are:
- That it makes your code less clear, and reduces readability.
- That it imposes a more functional way of thinking into your code design, which when combined with the other imperative aspects risks confusing design ideas.
- That it introduces a more syntax heavy style of coding.
Furthermore the fact that the body of a lambda
has to be a single expression (rather than a series of statements) may appear to place strict limits on how much logic you can access within the lambda
. However if you become familiar with how to construct lambda
expressions and where they can be used within your code you’ll find that most statements in Python can be re-expressed as lambda
expression equivalents. And so by extension, there are many occasions where lambda
functions provide clear value to the Python language and developers design ideas.
Shorthand constructs
lambda
expressions are regularly used with Python’s built in map()
, filter()
, and reduce()
functions. For example:
list(map(lambda x: x.upper(), ['cat', 'dog', 'cow']))
Returns a list of the form ['CAT', 'DOG', 'COW']
.
>>> list(filter(lambda x: 'o' in x, ['cat', 'dog', 'cow']))
Returns a list of the form ['dog', 'cow']
.
>>> from functools import reduce
>>> reduce(lambda acc, x: f'{acc} | {x}', ['cat', 'dog', 'cow'])
Returns the string construct cat | dog | cow
.
Another example involves embedding lambda
functions directly into a Python list. These are often called jump tables, that is, lists or dictionaries of actions to be performed on demand. For example:
myList = [ lambda x: x ** 2,
lambda x: x ** 3,
lambda x: x ** 4]
And we might use the above construct as follows:
for func in myList:
print(func(2))
Which will print 4, 8, 16
.
print(myList[0](3))
Which will print 9
.
Key Functions
Key functions are higher-order functions that take a parameter key as a named argument. Thus the key receives a function that can be a lambda
. This function will then directly influence the algorithm driven by the key function itself.
For example:
sort() # list method
sorted(), min(), max() # built-in functions
And thus, imagine that you wanted to sort a list of IDs, which were represented as strings - where each ID was formed as the concatenation of the string ‘id’ and some number:
ids = ['id1', 'id2', 'id30', 'id3', 'id22', 'id100']
If you were to sort this list with the built-in function sorted()
, then by default it would return the list sorted in lexicographic order because the elements in the list are strings. Thus ‘a’ before ‘b’ before ‘c’, etc:
print(sorted(ids)) # Lexicographic sort
Would return the list in lexicographic order: ['id1', 'id100', 'id2', 'id22', 'id3', 'id30']
.
But by using lambda
expressions we can influence how the sorting process is executed. You can assign a lambda
function to the named argument key
such that the sorting will use the number associated with the ID:
sorted_ids = sorted(ids, key=lambda x: int(x[2:])) # Integer sort
Which would return: ['id1', 'id2', 'id3', 'id22', 'id30', 'id100']
.
Higher-Order Functions
lambda
functions are frequently used with higher-order functions, which take one or more functions as arguments or return one or more functions.
A lambda
function can be used as a higher-order function by taking a function (normal or lambda) as an argument - such as in the following example:
high_ord_func = lambda x, func: x + func(x)
high_ord_func(2, lambda x: x * x) # Returns 6.
high_ord_func(2, lambda x: x + 3) # Returns 7.
And as we’ll see in a moment, Python provides a few built in higher-order functions in the standard library. There include map()
, filter()
, functools.reduce()
, as well as key functions like sort()
, sorted()
, min()
, and max()
.
Map
The built-in map()
higher-order function takes a function as its first argument and applies this to each of the elements of its second argument, which must be an iterable. Examples of iterables are strings, lists, and tuples. Thus map()
will return an iterator corresponding to the transformed collection. For example suppose you wanted to transform a list of strings into a new list with each string capitalized - you could use map()
as below:
list(map(lambda x: x.capitalize(), ['cat', 'dog', 'cow']))
Which would return ['Cat', 'Dog', 'Cow']
.
Filter
The built-in filter()
higher-order function can be converted into a list comprehension. It takes a predicate as a first argument and an iterable as a second argument and then builds an iterator containing all the elements of the initial collection that satisfy the predicate function. For example, to filter all the even numbers out of a given list of integers, we can do the following:
# The predicate.
even = lambda x: x%2 == 0
# The filter() function:
list(filter(even, range(11)))
Which would return the even numbers only as a new list: [0, 2, 4, 6, 8, 10]
.
Reduce
Since Python 3, reduce()
moved from being a built-in function to a function contained within the functools
module. As was the case with map()
and filter()
, its first two arguments are respectively a function and an iterable. It may also take an initializer as a third argument that is used as the initial value of the resulting accumulator.
For each element of the iterable, reduce()
applies the function and accumulates the result that is returned when the iterable is exhausted. For example we could apply reduce()
to a list of pairs and calculate the sum of just the first item of each pair, as follows:
import functools
pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
functools.reduce(lambda acc, pair: acc + pair[0], pairs, 0)
Which would return the sum of the first item of each pair: 6
.
Conclusions
So, if you bind a lambda
function to a variable you may well wonder what the exactly distinguishes this from a regular function with a single return
statement.
add = lambda x, y: x + y
# versus
def add(x, y):
return x + y
And the answer is that behind the scenes there is indeed almost no difference between these two. But as we have seen throughout the article, the value of lambda
expressions lies in being able to place them in locations where the regular def
statement is not allowed. For example to define inline callback functions, or for rapid coding and placement of snippet functionality directly where it is needed rather than requiring it to be defined somewhere else as with the def
statement. And finally, in the case of the map()
, filter()
, and reduce()
methods, when these are nested within a list comprehension (the square [
]
brackets) we obtain multiple functionality from single lines of code. Even managing to return to more Pythonic ways of designing code!