Swami Vivekananda

What are lambda functions?

27 Feb 2021 - fubar - Sreekar Guddeti

python-Logo 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.

Abstract

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.

Anonymous functions

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

Lambda is an expression and not statement

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)>]

General purpose multi-branching tool

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. Lambdas 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

Limitation: lambda is a single line of expression

However, 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.

Inline function definition

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()

Functional programming

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                                    

Impact of lambda on Zen of Python idioms

Upholds Beautiful 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.

Breaks explicit is better than implicit

lambdas 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.

Breaks 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.

Conclusion

Further reading

Assets

print_as_table.py