27 Feb 2021 - fubar - Sreekar Guddeti
In contrast to the usual way of defining functions in Python by using the def statement, lambda function is defined as an expression. In this post, we will compare these contrasting ways and explore the possibilities and limitations of this way of function definition.
In addition to the def
statement, Python offers another peculiar way of defining functions using lambda
. The word lambda
is inherited from lambda calculus used predominantly in the symbolic expression based progamming language Lisp. To understand lambda
’s, it is convenient to compare with the def
statement.
While def
is a statement where function definition encapsulated within the nested block gets assigned to the function name function_name
, as in
def function_name(argument_names):
block
statement
defining
the
function
, lambda is an expression; it is only evaluated (at run time) and does not get assigned to any name per se. As this is a nameless “definition”, lambda is called an anonymous function. However, we can always assign it a name; in that case the name refers to the function “definition” as in
In [123]: lambda x: x**2
Out[123]: <function __main__.<lambda>(x)>
In [124]: f = lambda x: x**2
In [125]: f(2)
Out[125]: 4
As lambda is an expression, just like other expressions as in
In [126]: True == False
Out[126]: False
In [127]: True + False
Out[127]: 1
, it can be used in places where expressions are allowed but not statements like in sequences (lists, dictionaries), as in
In [135]: functions = [lambda x: x**2, lambda x: x**3]
In [136]: functions
Out[136]: [<function __main__.<lambda>(x)>, <function __main__.<lambda>(x)>]
To branch the flow depending on a condition,
we can use if-elif-else
statement. However, for many branches, it becomes cumbersome coding. An alternative is the switch-case
C++ statement is lacking in Python. Lambda
s can be used to implement a switch-case like behavior by combining with dictionary.
cases = {'multiply': lambda x: x*x,
'power': lambda x: x**x,
'add': lambda x: x+x,
'substract': lambda x: x-x,
'divide': lambda x: x
}
switch = 'power'
# Get the action connected to the switch
action = cases[switch]
# Perform the action
In [153]: action(3)
Out[153]: 27
lambda
is a single line of expressionHowever, one downside of being an expression, lambda
should be expressed within a single line. In contrast, statements like def
, if-else
can afford block of lines.
Because it is an expression, lambda
can be used for in-line function definition. This can be useful in cases like:
import tkinter as tk
x = tk.Button(
text ='Press me',
command=(lambda:print('Spam')))
x.pack()
tk.mainloop()
In addition to other functional programming tools (see Functional Programming in Python) like map()
and zip()
,
we can reimplement most statements as expression. For example, for
loops as in
# %% Print formatted table (versions).
def formatter_loop(_list, padding):
"""
Print the `_list` with formatting
such as flexible `padding`
"""
if debug: print('Formatting by formatter_loop()')
for row in _list:
# Initialize an empty formatted-row-string
formatted_row_string = ''
for col in row:
# Format the column.
formatted_col = f'{col:>{padding}}'
# Concatenate to formatted-row-string.
formatted_row_string += formatted_col
# Print the formatted-row-string.
print(formatted_row_string)
The inner for loop can be implemented using map()
as
def formatter_map(_list, padding):
"""
Use functional programming tools `map()` to condense
the code.
"""
if debug: print('Formatting by formatter_map()')
# Get the number of columns.
ncols = len(_list[0])
# Define list of paddings.
paddings = [padding for i in range(ncols)]
# Define custom str() method to return formatted string
def my_str(_str, padding):
"""
Format `_str` with flexible `padding`.
"""
return f'{_str:>{padding}}'
for row in _list:
# Map `my_str()` to the iterable `row`.
# The return is an iterable of strings.
# Interleave the strings with `join()` of empty string ''.
print(''.join(map(my_str, row, paddings)))
Here we have used an enclosed helper function. However, a more concise/beautiful implementation is to define the helper function using lambda
as in
def formatter_lambda(_list, padding):
"""
Let us circumvent the limitation in version 3 of not able
to provide `padding` as an argument by using lambda function.
`map()` requires a function object as the first argument.
`lamda` can take more than one argment and return a function object on the fly (runtime). Let us club these tools to avoid
the `def` statement of helper function mystr().
"""
if debug: print('Formatting by formatter_lambda()')
# Get the number of columns.
ncols = len(_list[0])
# Define list of paddings.
paddings = [padding for i in range(ncols)]
for row in _list:
print(''.join(map(lambda _str, padding: f'{_str:>{padding}}', row, paddings)))
Let us test these different implementations
# %% Test the above functions.
header = [
['NN', 'act. fn.', 'cost fn.', 'optimizer'],
['Sequential', 'linear', 'quadratic', 'none'],
['Convolutional', 'sigmoid', 'softmax', 'adam'],
['Autoencoder', 'ReLU', '', ''],
]
print('='*15)
print('Using version 1')
print('='*15)
formatter_loop(header, 12)
print('='*15)
print('Using version 2')
print('='*15)
formatter_map(header, 15)
print('='*15)
print('Using version 3')
print('='*15)
formatter_lambda(header, 18)
gives as output
In [148]: runcell('Test the above functions.', 'E:/Databases/PythonProjects/FunctionalProgramming/print_as_table.py')
===============
Using version 1
===============
NN act. fn. cost fn. optimizer
Sequential linear quadratic none
Convolutional sigmoid softmax adam
Autoencoder ReLU
===============
Using version 2
===============
NN act. fn. cost fn. optimizer
Sequential linear quadratic none
Convolutional sigmoid softmax adam
Autoencoder ReLU
===============
Using version 3
===============
NN act. fn. cost fn. optimizer
Sequential linear quadratic none
Convolutional sigmoid softmax adam
Autoencoder ReLU
lambda
on Zen of Python idiomsBeautiful is better than ugly
Though beauty is subjective, once we begin to appreciate the lambda
, we cannot ignore its beauty in terms of conciseness, brevity. For example, in the formatter example, the first two (loop-, map-) implementations needed 5 lines, while the lambda-implementation needed 4 lines.
explicit is better than implicit
lambda
s encourage concise coding. However the downside is obfuscation of code. Code becomes harder to read. This violates the Explicit is better than implicit
idiom of the Zen of Python.
Flat is better than nested
As we have seen in the call back handler or in the formatter example, we have nested functions. This creates reading difficulty as Flat is better than nested
.
lambda
s are expressions. So they are useful when the function logic can be implemented within a single line of expression. For extended logic, def
is the way to go.
Notwithstanding the subjective beauty associated with brevity, conciseness while using lambda
s, these do not honour many of the idioms of the Zen of Python.
lambda
s enable switch-case like C++ statement by listing them in a dictionary. This allows a cleaner multi-branching tool than if-elif-else
statement.
lambda
s offer in-line function definition. This feature is useful in embedding code where it is needed, instead of far-away location in the code pool. This helps reading code in a linear manner.
In addition to other functional programming tools, lambda
s leverage the functional programming paradigm of Python