and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with Invariant Sections being Forward, Prefaces, and Contributor List, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled “GNU Free Documentation License”.
2.2. Saving Expressions for Later Execution - The lambda Expression¶
2.2.1. Abstraction with Variables and Expressions¶
The most important concept in programming is abstraction. Creating an abstraction involves replacing code with a name, which replaces the complexity of the solution with the meaning of the code. In this way, a programmer can deal with a complex problem by combining many small and easy to understand abstractions. This process is added by picking names that keep the meaning of the code intact while hiding the implementation details (complexity), which is why we will be so particular about the names that we use.
We have seen our first form of abstraction in the form of a variable. Using variables allows us to replace values with a name that expresses the meaning of these values in the context of the problem. Consider the following calculation.
In [1]: int(100*(1.55*1.065*3))/100
While this expression is easy to understand arithmetically, it lacks any real-world meaning. Compare this to the following code, which has applied the variable abstraction to give the code meaning.
In [1]: iced_tea_price = 1.55
In [1]: tax_rate = 1.065
In [1]: number_of_glasses = 3
In [1]: total_cost = round(iced_tea_price*tax_rate*number_of_glasses, 2)
In [1]: total_cost
Now we see the meaning behind each of the numbers. Here we have combined the
variable abstraction (represented by the variables iced_tea_price
,
tax_rate
, number_of_glasses
and total_cost
) with the round
function, which is another form of abstraction. In particular, the round
function makes the meaning of the int(100*(...))/100
portion of the
expression much easier to understand by hiding this complexity behind a
meaningful name.
2.2.2. The Generalization and the DRY Principle¶
Abstractions allow us to introduce two other programming important concepts:
generalization and the “Don’t Repeat Yourself” (DRY) principle. Code
that has been generalized using abstractions can be used to solve not just one
problem but many problems. An example of the application of generalization
is the use the round
function from the last section. Not only can we use
this function to round to two decimal places when working with dollars and
cents, it generalizes the process of rounding to any number of decimal places.
In [1]: from math import pi
In [1]: round(pi)
In [1]: round(pi, 3)
In [1]: round(pi, 7)
Adhering to the DRY principle is a standard approach to writing code that is
correct (e.g. computes the correct values), clean (easy to read) and
maintainable (can be changed or extended). We will strive to eliminate all
repeated code in our programs. One advantage to this approach is
increased productivity, since we won’t waste time retyping the same code over and over.
Another, perhaps more important, advantage is that any mistakes that we make
will be isolated to one place in the code. Functions will be our primary tool
for abstraction, generalization, and the elimination of replicated code. In
this next section, we will introduce the lambda
expression, which will allow
us to save expressions for later execution, as well as generalize the solution
to a problem.
2.2.3. The lambda Expression¶
While the iced tea example from above illustrated the power of abstraction to compartmentalize complexity, it was not a general solution to the problem. Suppose that we want to compute the cost (with tax) of purchasing 2 and 5 glasses of tea. Currently, we would accomplish this as follows.
In [1]: number_of_glasses = 2
In [1]: total_cost = round(iced_tea_price*tax_rate*number_of_glasses, 2)
In [1]: total_cost
In [1]: number_of_glasses = 5
In [1]: total_cost = round(iced_tea_price*tax_rate*number_of_glasses, 2)
In [1]: total_cost
We have violated the DRY principle by using the
round(iced_tea_price*tax_rate*number_of_glasses, 2)
expression twice! We
can make a more general solution using the lambda
expression, which allows
us to turn simple expressions into functions. The syntax for the one variable
lambda
expression is as follows:
lambda PARAMETER: BODY_EXPRESSION
Note
We have specialized language when talking about lambda expressions (and other functions.) The variable that the we are generalizing is called the formal parameter and the expression that we mean to evaluate is referred to as the body of the lambda expression.
Generalizing the application of the tax rate to the cost of an item would be a
useful abstraction, as this function could be applied in many situations. The
general expression of applying tax is tax_rate*cost
. We want to be able to
change the value of cost
in our generalization, which is accomplished using a
lambda expression as shown below
In [1]: apply_tax = lambda cost: round(tax_rate*cost, 2)
In [1]: apply_tax
Notice that we can save lambda expressions to variables in the same way that we save any other value. Simply evaluating the lambda expression does not execute the encapsulated code, to do this we need to use a new form of syntax.
Once a lambda expression is constructed, it can be applied later to a given value of the variable using the following syntax.
LAMBDA_EXPRESSION(ARGUMENT)
LAMBDA_EXPRESSION can be the literal expression or a variable to which the expression was saved. For simple lambda expressions, all instances of PARAMETER will be replaced with the value of the ARGUMENT, and then evaluate the BODY_EXPRESSION*.
Note
*Since lambda expressions are themselves expressions, we can embed lambda expressions inside lambda expressions. This will cause problems with the simple substitution rule that we gave above. More on this complexity later.
In [1]: apply_tax = lambda cost: round(tax_rate*cost, 2)
In [1]: total_cost = apply_tax(iced_tea_price*number_of_glasses)
In [1]: total_cost
Hard-coding the tax rate inside apply_tax
makes this a less general solution
to the problem. We can fix this by adding another parameter to the lambda
expression. To do this, we will need to allow two variables in the body to
change from problem to problem, which can also be accomplished with the lambda
expression by adding additional parameters. The syntax for the more general
lambda expression, with many parameters, is as follows.
lambda PARAMETER_1, ... , PARAMETER_K: BODY_EXPRESSION
We can apply this more general expression using a similar application syntax.
LAMBDA_EXPRESSION(ARGUMENT_1, ..., ARGUMENT_K)
Note that the number of arguments needs to match the number of arguments
In [1]: pow = lambda x, y: x**y
In [1]: pow
In [1]: pow(2,3)
In [1]: pow(2)
In [1]: pow(1,2,3)
Returning to the iced tea example, we now make a more general version of
apply_tax
.
In [1]: apply_tax = lambda cost, tax: round(tax*cost, 2)
In [1]: pre_tax_cost = iced_tea_price*number_of_glasses
In [1]: total_cost = apply_tax(pre_tax_cost, tax_rate)
In [1]: total_cost
To continue the generalization process, we construct a lambda expression for computing the (pre_tax) cost of any number of glasses of tea and use it to compute the cost for 3, 2, and 5 glasses of tea.
In [1]: pre_tax_cost = lambda num_glasses: iced_tea_price*num_glasses
In [1]: apply_tax(pre_tax_cost(3), tax_rate)
In [1]: apply_tax(pre_tax_cost(2), tax_rate)
In [1]: apply_tax(pre_tax_cost(5), tax_rate)
Question
Do you see where we have violated the DRY principle? How might we fix this problem?
Finally, we will eliminated the repeated code and hide the complexity of the computation of the total cost in another lambda expression.
In [1]: total_cost = lambda num_glasses: apply_tax(pre_tax_cost(num_glasses), tax_rate)
In [1]: total_cost(3)
In [1]: total_cost(2)
In [1]: total_cost(5)
To better understand the evaluation of this code, use codelens to step through the evaluation step-by-step.
This example includes many of the ideas that we will explore in a later chapter on functional programming, including
- Abstracting constants with global variables. In this case, the values of
iced_tea_price
andtax_rate
have been assigned to variables that are used by other functions and expressions. Doing this at the top of our code allows us to easily change these constants at a later date. - Abstracting expressions using functions by pulling out variables as formal parameters. We can think of these parameters as “holes” in our program that can be filled in later.
- Composing small functions to solve more complicated problems. Notice that
total_cost
is an example of function composition, asapply_tax
is applied to the result ofpre_tax_cost
.
2.2.4. Boolean Functions¶
We have already seen that boolean values result from the evaluation of boolean expressions. Since the result of any expression evaluation can be returned by a lambda expression, lambda expressions can return boolean values. This turns out to be a very convenient way to hide the details of complicated tests. For example:
In [1]: isDivisible = lambda x, y: True if x % y == 0 else False
In [1]: isDivisible(10, 5)
The name of this function is isDivisible
. It is common to give boolean
functions names that sound like yes/no questions. isDivisible
returns
either True
or False
to indicate whether the x
is or is not
divisible by y
.
We can make the lambda expression more concise by taking advantage of the fact
that the condition of the if
expression is itself a boolean expression. We
can return it directly, avoiding the if
statement altogether:
In [1]: isDivisible = lambda x, y: x % y == 0
In [1]: isDivisible(10, 5)
Boolean functions are often used in conditional statements:
EXPR_1 if isDivisible(x, y) else EXPR_2
Note
It might be tempting to write something like isDivisible(x, y) == True
as
the condition of the if expression, but the extra comparison is not necessary.
The following example shows the isDivisible
function at work. Notice how
descriptive the code is when we move the testing details into a boolean
function, which is a direct result of using an abstraction. Try it with a few
other actual parameters to see what is printed.
In [1]: isDivisible = lambda x, y: x % y == 0
In [1]: "That works" if isDivisible(10, 5) else "Those values are no good"
Here is the same program in codelens. When we evaluate the if
statement in
the main part of the program, the evaluation of the boolean expression causes a
call to the isDivisible
function. This is very easy to see in codelens.
Check your understanding
-
data-2-13: What is a Boolean function?
- (A) A function that returns True or False
- A Boolean function is just like any other function, but it always returns True or False.
- (B) A function that takes True or False as an argument
- A Boolean function may take any number of arguments (including 0, though that is rare), of any type.
- (C) The same as a Boolean expression
- A Boolean expression is a statement that evaluates to True or False, e.g. 5+3==8. A function is a series of expressions grouped together with a name that are only executed when you call the function.
- (A) Yes
- It is perfectly valid to return the result of evaluating a Boolean expression.
- (B) No
- x +y < z is a valid Boolean expression, which will evaluate to True or False. It is perfectly legal to return True or False from a function, and to have the statement to be evaluated in the same line as the return keyword.
data-2-14: Is the following statement legal in Python (assuming x, y and z are defined to be numbers)?
return x + y < z
Note
This workspace is provided for your convenience. You can use this activecode window to try out anything you like.
2.2.5. Aside: Lambda Calculus and the Power of the Lambda Expression¶
While the expressions that we have introduced so far may seem like a very small subset of a programming language like Python, it turns out that they are very powerful. Alonzo Church developed the lambda calculus in 1930 as a general method of describing computation. The lambda calculus consists of three basic building blocks.
- variables
- lambda expressions/abstractions
- an application rule for lambda expressions
It has been shown that this system is Turing complete, meaning that it can be used to describe every possible computation that can be performed on a computer. Thus we could simply write all of our programs with the tools presented so far ... but we won’t! Programs written in this style are unnecessarily complicated without introducing many of the abstractions that are already provided by Python. For more information about the lambda calculus, visit the Wikipedia page.